merge(13): AIUI source migrated in-repo — supersedes the two-repo split (D-15/D-18)

Brings AIUI's full 230-commit history under aiui/ via git subtree, plus main's
current head. Operator decision 2026-08-03: AIUI moves into this repo rather
than staying at git.tx1138.com. This also lands e30ac1d (13-01 Task 3), which
was stranded local-only while that remote was unreachable.

Plans 13-06, 13-09 and 13-11 still target /home/archipelago/Projects/AIUI paths
and must be re-planned against aiui/ before wave 2 runs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-08-03 15:11:26 -04:00
co-authored by Claude Opus 5
407 changed files with 69788 additions and 32 deletions
@@ -486,6 +486,18 @@ describe('RPCClient convenience methods', () => {
expect(getLastMethod()).toBe('federation.invite')
})
it('federationInvite omits password when none is given', async () => {
mockSuccess({ code: 'ABC', did: 'did:key:z', onion: 'abc.onion' })
await rpcClient.federationInvite('observer')
expect(getLastParams()).not.toHaveProperty('password')
})
it('federationInvite forwards the password for a trusted invite', async () => {
mockSuccess({ code: 'ABC', did: 'did:key:z', onion: 'abc.onion' })
await rpcClient.federationInvite('trusted', 'hunter2')
expect(getLastParams()).toMatchObject({ trust_level: 'trusted', password: 'hunter2' })
})
it('federationJoin calls federation.join', async () => {
mockSuccess({ joined: true, node: {} })
await rpcClient.federationJoin('invite-code')
@@ -510,6 +522,22 @@ describe('RPCClient convenience methods', () => {
expect(getLastMethod()).toBe('federation.set-trust')
})
it('federationSetTrust omits password on demotion', async () => {
mockSuccess({ updated: true, did: 'did:key:z', trust_level: 'observer' })
await rpcClient.federationSetTrust('did:key:z', 'observer')
expect(getLastParams()).not.toHaveProperty('password')
})
it('federationSetTrust forwards the password when promoting', async () => {
mockSuccess({ updated: true, did: 'did:key:z', trust_level: 'trusted' })
await rpcClient.federationSetTrust('did:key:z', 'trusted', 'hunter2')
expect(getLastParams()).toMatchObject({
did: 'did:key:z',
trust_level: 'trusted',
password: 'hunter2',
})
})
it('federationSyncState calls federation.sync-state', async () => {
mockSuccess({ synced: 1, failed: 0, results: [] })
await rpcClient.federationSyncState()
+15 -3
View File
@@ -781,12 +781,18 @@ class RPCClient {
}
// Federation
/** Minting a `trusted` invite requires the node password — the backend
* rejects it with a `PASSWORD_REQUIRED` error until one is supplied.
* Observer invites never need one. */
async federationInvite(
trustLevel: 'trusted' | 'observer' = 'trusted'
trustLevel: 'trusted' | 'observer' = 'trusted',
password?: string,
): Promise<{ code: string; did: string; onion: string; trust_level: string }> {
const params: Record<string, unknown> = { trust_level: trustLevel }
if (password) params.password = password
return this.call({
method: 'federation.invite',
params: { trust_level: trustLevel },
params,
})
}
@@ -855,13 +861,19 @@ class RPCClient {
})
}
/** Promotion TO `trusted` requires the node password — the backend rejects
* it with a `PASSWORD_REQUIRED` error until one is supplied. Demotion is
* never gated: making a peer less privileged must stay easy. */
async federationSetTrust(
did: string,
trustLevel: 'trusted' | 'observer' | 'untrusted',
password?: string,
): Promise<{ updated: boolean; did: string; trust_level: string }> {
const params: Record<string, unknown> = { did, trust_level: trustLevel }
if (password) params.password = password
return this.call({
method: 'federation.set-trust',
params: { did, trust_level: trustLevel },
params,
})
}
+91 -10
View File
@@ -223,6 +223,15 @@
@confirm="confirmPresenceSign"
@cancel="showPresenceSignModal = false"
/>
<TrustPasswordModal
:visible="showTrustPassword"
:context="trustPasswordContext"
:busy="trustPasswordBusy"
:error="trustPasswordError"
@confirm="submitTrustPassword"
@close="closeTrustPassword"
/>
</div>
</template>
@@ -243,9 +252,10 @@ import JoinModal from './federation/JoinModal.vue'
import PendingRequestsPanel from './federation/PendingRequestsPanel.vue'
import DiscoverModal from './federation/DiscoverModal.vue'
import PresenceSignModal from './federation/PresenceSignModal.vue'
import TrustPasswordModal from './federation/TrustPasswordModal.vue'
import type { FederatedNode, DwnStatus, SyncResult } from './federation/types'
import type { PendingPeerRequest } from '@/api/rpc-client'
import { nodeName, timeAgo } from './federation/utils'
import { nodeName, nodeNameFromDid, timeAgo } from './federation/utils'
const transportStore = useTransportStore()
const appStore = useAppStore()
@@ -529,15 +539,73 @@ function handleGenerateInvite(type: 'trusted' | 'observer') {
generateInvite()
}
/** The backend is the only authority on whether a given change is an
* escalation, so the UI never pre-judges: it attempts the call and prompts
* only when the backend says a password is required. That keeps demotions
* and no-op re-sets of an already-Trusted peer free of a pointless prompt
* without the frontend having to duplicate the rule. */
function isPasswordRequired(e: unknown): boolean {
return e instanceof Error && e.message.includes('PASSWORD_REQUIRED')
}
const showTrustPassword = ref(false)
const trustPasswordContext = ref('')
const trustPasswordBusy = ref(false)
const trustPasswordError = ref('')
let pendingTrustAction: ((password: string) => Promise<void>) | null = null
function promptForTrustPassword(context: string, action: (password: string) => Promise<void>) {
trustPasswordContext.value = context
trustPasswordError.value = ''
pendingTrustAction = action
showTrustPassword.value = true
}
function closeTrustPassword() {
showTrustPassword.value = false
trustPasswordError.value = ''
trustPasswordBusy.value = false
pendingTrustAction = null
}
async function submitTrustPassword(password: string) {
if (!pendingTrustAction) return
try {
trustPasswordBusy.value = true
trustPasswordError.value = ''
await pendingTrustAction(password)
closeTrustPassword()
} catch (e) {
// Keep the failure inside the modal so the operator can retry in place
// rather than losing the pending action to the page-level banner.
trustPasswordError.value = e instanceof Error ? e.message : 'Password verification failed'
} finally {
trustPasswordBusy.value = false
}
}
/** Raw call throws so both the first attempt and the password retry can
* route the error to the right place. */
async function requestInvite(password?: string) {
// The invite type is not cosmetic: it sets the trust level the invite
// grants both sides ("Invite a Peer" = observer, "Link Your Nodes" = trusted)
const result = await rpcClient.federationInvite(inviteType.value, password)
inviteCode.value = result.code
}
async function generateInvite() {
try {
generatingInvite.value = true
error.value = ''
// The invite type is not cosmetic: it sets the trust level the invite
// grants both sides ("Invite a Peer" = observer, "Link Your Nodes" = trusted)
const result = await rpcClient.federationInvite(inviteType.value)
inviteCode.value = result.code
await requestInvite()
} catch (e) {
if (isPasswordRequired(e)) {
promptForTrustPassword(
'This invite grants Trusted access to whoever redeems it — full read of this node\'s state, and the ability to deploy apps to it. Confirm with your node password.',
requestInvite,
)
return
}
error.value = e instanceof Error ? e.message : 'Failed to generate invite'
} finally {
generatingInvite.value = false
@@ -578,14 +646,27 @@ async function syncAll() {
}
}
/** Raw call — throws; see `requestInvite`. */
async function requestTrustChange(did: string, level: string, password?: string) {
await rpcClient.federationSetTrust(did, level as 'trusted' | 'observer' | 'untrusted', password)
await loadNodes()
if (selectedNode.value?.did === did) {
selectedNode.value = nodes.value.find(n => n.did === did) ?? null
}
}
async function changeTrust(did: string, level: string) {
try {
await rpcClient.federationSetTrust(did, level as 'trusted' | 'observer' | 'untrusted')
await loadNodes()
if (selectedNode.value?.did === did) {
selectedNode.value = nodes.value.find(n => n.did === did) ?? null
}
await requestTrustChange(did, level)
} catch (e) {
if (isPasswordRequired(e)) {
const name = nodeNameFromDid(did, nodes.value)
promptForTrustPassword(
`Granting ${name} Trusted lets it read this node's state and deploy apps to it. Confirm with your node password.`,
(password) => requestTrustChange(did, level, password),
)
return
}
error.value = e instanceof Error ? e.message : 'Failed to update trust level'
}
}
@@ -26,7 +26,7 @@
<div class="flex items-center gap-2 mt-1">
<select
:value="node.trust_level"
@change="emit('change-trust', node!.did, ($event.target as HTMLSelectElement).value)"
@change="onTrustChange"
class="bg-black/30 text-white text-sm rounded px-2 py-1 border border-white/10"
>
<option value="trusted">Trusted</option>
@@ -34,6 +34,9 @@
<option value="untrusted">Blocked</option>
</select>
</div>
<p class="text-xs text-white/40 mt-2">
<span class="text-white/30">Granted via:</span> {{ trustSourceLabel }}
</p>
</div>
<div class="bg-white/5 rounded-lg p-3">
<p class="text-xs text-white/40 mb-1">Added</p>
@@ -130,7 +133,7 @@
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { computed, ref } from 'vue'
import type { FederatedNode } from './types'
import { formatBytes, formatUptime } from './utils'
@@ -156,6 +159,32 @@ const emit = defineEmits<{
const confirmRemove = ref(false)
const deployAppId = ref('')
const TRUST_SOURCE_LABELS: Record<string, string> = {
invite: 'An invite you minted',
'uninvited-join': 'Joined without an invite — capped at Observer',
'transitive-merge': 'Advertised by another peer — capped at Observer',
manual: 'You set it here',
}
/** Unknown provenance is stated plainly rather than hidden: a peer recorded
* before this was tracked is precisely the one worth a second look. */
const trustSourceLabel = computed(
() => TRUST_SOURCE_LABELS[props.node?.trust_source ?? ''] ?? 'Unknown — recorded before this was tracked',
)
/** Snap the select back to the node's actual level immediately. Promoting to
* Trusted asks for the node password, and the operator may cancel or get it
* wrong without this the dropdown would keep displaying a level the node
* never accepted. On success the parent reloads and the prop drives the new
* value back in. */
function onTrustChange(event: Event) {
const select = event.target as HTMLSelectElement
const level = select.value
if (!props.node) return
select.value = props.node.trust_level
emit('change-trust', props.node.did, level)
}
function handleClose() {
confirmRemove.value = false
deployAppId.value = ''
@@ -0,0 +1,74 @@
<template>
<Teleport to="body">
<Transition name="modal">
<div v-if="visible" class="fixed inset-0 z-[3000] flex items-center justify-center p-4" @click.self="handleClose">
<div class="absolute inset-0 bg-black/60 backdrop-blur-sm"></div>
<div class="glass-card p-6 max-w-md w-full relative z-10">
<h3 class="text-lg font-semibold text-white mb-2">Confirm Trusted Access</h3>
<p class="text-sm text-white/60 mb-4">{{ context }}</p>
<input
ref="passwordInput"
v-model="password"
type="password"
autocomplete="current-password"
placeholder="Enter your node password to confirm"
class="w-full bg-black/30 border border-white/10 rounded-lg px-3 py-2 text-sm text-white placeholder-white/30 focus:outline-none focus:border-orange-500/50 mb-4"
@keyup.enter="submit"
/>
<p v-if="error" class="text-red-400 text-xs mb-3">{{ error }}</p>
<div class="flex gap-3">
<button @click="handleClose" class="flex-1 glass-button px-4 py-2 rounded-lg text-sm">Cancel</button>
<button
@click="submit"
:disabled="busy || !password"
class="flex-1 glass-button px-4 py-2 rounded-lg text-sm font-medium bg-orange-500/20 border-orange-500/30 disabled:opacity-50"
>
{{ busy ? 'Verifying…' : 'Grant Trusted' }}
</button>
</div>
</div>
</div>
</Transition>
</Teleport>
</template>
<script setup lang="ts">
import { ref, nextTick, watch } from 'vue'
const props = defineProps<{
visible: boolean
/** What is about to be granted, in the operator's terms. */
context: string
busy: boolean
error: string
}>()
const emit = defineEmits<{
close: []
confirm: [password: string]
}>()
const password = ref('')
const passwordInput = ref<HTMLInputElement | null>(null)
function submit() {
if (!password.value || props.busy) return
emit('confirm', password.value)
}
function handleClose() {
password.value = ''
emit('close')
}
// Never leave the password sitting in memory once the modal is dismissed,
// and put the cursor where the operator has to type anyway.
watch(() => props.visible, async (val) => {
if (!val) {
password.value = ''
return
}
await nextTick()
passwordInput.value?.focus()
})
</script>
+7
View File
@@ -40,6 +40,13 @@ export interface FederatedNode {
last_sync_error?: string
/** RFC 3339 timestamp of last_sync_error. */
last_sync_error_at?: string
/**
* How this peer's trust level came to be what it is. `null` means it was
* recorded before provenance was tracked which is exactly the population
* worth reviewing, since it may include grants made by the fail-open paths
* that `uninvited-join` / `transitive-merge` now cap at Observer.
*/
trust_source?: 'invite' | 'uninvited-join' | 'transitive-merge' | 'manual' | null
}
export interface DwnStatus {