1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
plan: "0038"
title: "iOS embedded kcp: the AT-IRC iOS app runs kcp and the socialweb requester in process, signs in with OAuth, and launches leased atirc agents through the market request flow"
status: doing
source: >
2026-10-30 atirc, owner: "see if you can get a build of kcp running in the ios app, we got it running
in wasm so this should be possible", then the full shell.socialweb.computer scope set and an Agents
section that launches agents "via the same market request flow ... via kcp the other agents are
working on". Lead and plan owner: linux-kcp. App owner: atirc-ios-client-dev (its own milestone plan
M1-M5: AT-IRC docs/plans/0001-ios-embedded-kcp.md in atirc-client-ios; bridge AtircgoKCPStart,
AtircgoKCPRequest, AtircgoKCPSnapshot, AtircgoKCPReady, AtircgoKCPStatus, AtircgoKCPStop in package
github.com/kcp-dev/kcp/contrib/wasm/ios/atircgo). Atirc server: atirc-dev. Port advice: wasm (the page
is the model).
principle: >
The iOS app is one more requester host, like the page: it composes the same MarketRequester root and
runs the same Requester component in process. No iOS-only market protocol, no iOS-only lexicon, no
socket listener in the app; the app reaches the market only through the requester's outbound XRPC and
the firehose (comms mode Firehose, as the page). The selected policy mode runs for every decision.
decisions:
runtime: >
One Go runtime per process: AtircGo.xcframework holds the app's Go library and the kcp bridge
(two gomobile frameworks crashed with two Go runtimes). Build GOOS=ios GOARCH=arm64 CGO_ENABLED=1
with -trimpath -ldflags "-s -w"; GOGC 25 and a GOMEMLIMIT; the apiserver watch cache off.
storage: >
kcp runs on the in-memory storage of kcp-wasm contrib/wasm/storage (no etcd), persisted to the app
container through its snapshot/restore and persist hooks; the restore check is the page's (exact
seqs both ways, key and owner).
oauth: >
The app has its own client metadata. Its scope is exactly the requester scope generated by
socialweb cmd/requester-scope at the socialweb rev the app is built at, plus
rpc:chat.atirc.revoke?aud=did:web:irc.fedfork.com%23atirc. Granted scopes are read from the token
response, never from the access token's opaque scope claim. Simulator sign-ins are scripted with the
alice credential file sourced into the one command that signs in; no sign-in URL is ever posted.
agents: >
The Agents section follows the page: one EphemeralInfra per app session, one ComputeContract entry
per launch with spec.agent (and guestTools for R2), the requester mints a leased atirc token after
the verified receipt, and Release stamps releaseRequestedAt. A release asked for before the app is
suspended finishes on the next launch from the persisted state; the app asks iOS for background
time to finish it first.
policy: >
The requester runs requester-tangled-vouch by default, as on every host. iOS has no deno: the policy
sandbox needs an iOS provider on PolicyEngine spec.sandbox (the plan 0037 pattern: a contract runner
with a Prober, Fenced only after the probe passes); until it exists the requester fails closed and
Request VM shows why.
policy_sandbox_api: >
PolicySandboxConfig.Provider gains "embedded" with Embedded *PolicyEmbeddedSandboxConfig
{TimeoutSeconds default 120, minimum 1}; the root PolicyEngineSandboxSettings gain the same value
and Embedded {TimeoutSeconds}. "embedded" names what socialweb sees: a JavaScript sandbox the app
that embeds the requester provides through pkg/embedded. The JS engine is the app's choice and the
probe proves it; the API does not name it. On ios pkg/embedded always selects provider embedded
for the requester's PolicyEngine; other platforms keep deno unless the app passes a host.
policy_sandbox_host: >
pkg/embedded.PolicySandbox (alias of internal/sandbox/embedded.Host), set on Config.PolicySandbox,
gomobile types only: Run(requestJSON []byte) ([]byte, error); Kill(runID string);
PlantProbe(marker string) ([]byte, error); ClearProbe(marker string) error. gomobile binds one
package, so the app's bridge package (contrib/wasm/ios/atircgo) declares the same method set and
passes its value to Config.PolicySandbox. Go calls every method from a Go thread and Run blocks
until the program ends: the app must never block its main thread on it. Go runs one program at a
time; Kill may arrive while Run is blocked and must make that Run return. Run request JSON is
{id, source, env, timeoutMs}, the answer {exitCode, stdout, stderr, timedOut}, the page bridge's
shapes. Run returns an error only when the host could not run the program; a failing program is
an answer with a nonzero exitCode. The host ends a run at timeoutMs with timedOut true and
exitCode -1; Go waits 5 s more, then calls Kill and reports timedOut itself. stdout and stderr are
capped at 64 KiB each (Go caps again).
policy_sandbox_program: >
source is one ES module with deno semantics, as the page bootstrap of scripts/browser/
policy_sandbox.js (kcp-wasm) gives it: static imports of node:buffer, node:crypto and
node:dns/promises resolve to that file's shims, top-level await, and the module's end exits 0.
Globals: the dedicated Worker set (fetch, AbortController, setTimeout, TextEncoder, TextDecoder,
atob, URL, crypto.getRandomValues and crypto.randomUUID), console.log/info/debug to stdout and
warn/error/trace to stderr, and Deno {env from the request env, exit(code), stdout.writeSync,
stderr.writeSync, inspect, errors.NotCapable, readTextFile(Sync) and writeTextFile(Sync) that
reject with NotCapable}. An uncaught error or rejection writes "error: ..." to stderr and exits 1.
Every run gets a fresh global scope and storage: nothing a run leaves behind is visible to the
next. The program source is never exposed as a global.
policy_sandbox_fence: >
The fence denies policy code the app's authority: the Keychain, the app container files (and
every file:// URL), UserDefaults, the app's HTTP cookies and credential storage, the Go runtime
and the kcp it runs (no native bridge object of any name in the JS global scope), any DOM, and
state across runs. Network is allowed, as --allow-net on Linux: fetch of https with no cookies or
stored credentials (an ephemeral session). Recommended host: a WKWebView with
WKWebsiteDataStore.nonPersistent() that runs the policy_sandbox.js frame unchanged (opaque-origin
srcdoc iframe, one module Worker per run, Worker.terminate at the deadline), driven by
callAsyncJavaScript in a private WKContentWorld, with no WKScriptMessageHandler or
WKURLSchemeHandler in the page world; the page sandbox already passed plan 0037 W0 on iPhone
Safari. A bare JavaScriptCore JSContext must add fetch, timers, modules and the Worker globals
itself and has no public API to stop an endless loop, so it fails the timeout fact unless every
run gets its own killable context.
policy_sandbox_probe: >
The Go Prober decides; the app only plants. Go makes a random marker and calls PlantProbe(marker);
the app stores the marker as a Keychain generic password (service socialweb-fence-probe), a
UserDefaults key socialweb-fence-probe, an HTTPCookieStorage.shared cookie socialweb-fence-probe
(domain socialweb-fence-probe.invalid) and a file socialweb-fence-probe.js in its container whose
exact content is: export default "<marker>"; and answers {engine, fileURL (the file:// URL of
that file), planted {keychain, file, defaults, cookies: "yes" or why not}}. Go then runs four
programs through Run: (1) the fence program, which must see Deno.env.get("SOCIALWEB_FENCE_ENV")
equal to the env it was given, import node:buffer, have fetch, see no document, window, parent,
top, localStorage or sessionStorage, see none of webkit, require, process, Bun,
__socialwebSandbox, AtircGo, Atircgo, Go; fail to read the planted file by fetch,
XMLHttpRequest and import(), and file:/// by fetch; find the marker in no string reachable from globalThis (own
properties, depth 4); and stash a second token in globalThis, IndexedDB and Cache Storage where
they exist; (2) for (;;) {} with timeoutMs 1000, which the host must end itself with timedOut
true; (3) a rerun that must see none of the stash, print to stdout and stderr and exit 3 through
Deno.exit; (4) a top-level throw that must exit 1. Then ClearProbe(marker) removes all four
plants. Fenced=True only when every plant is "yes" and every fact holds; the facts are the
Fenced condition message. A plant, run or answer error is Fenced=False, Ready=False, Deny.
state_rules:
- "No contract is created while the requester policy cannot run; Fenced=False means Deny."
- "The app never holds a long atirc token for agents; agent tokens are leased and die with the contract."
- "The account-level OAuth session is the owner identity for chat.atirc.answer and chat.atirc.owner_mode."
items:
- id: I1-BOOT
title: "kcp boots in process in the iOS Simulator (memory storage, net.Pipe, no sockets); status bar shows kcp ready"
owner: atirc-ios-client-dev
status: done
- id: I2-RUNTIME
title: "One Go runtime: AtircGo.xcframework (app golib + kcp bridge); memory settings (GOGC, GOMEMLIMIT, watch cache off) measured on a device"
owner: atirc-ios-client-dev
status: done
- id: I3-PERSIST
title: "Persistence across app restarts: snapshot/restore and persist hooks into the app container; cold boot vs resume times measured"
owner: atirc-ios-client-dev
status: done
- id: I4-SOCIALWEB-IOS
title: "socialweb builds for ios/arm64: the MarketRequester root and Requester component compose in the app's kcp (comms Firehose, no ingress), with ios build tags for host-only packages; build gate covers ios/arm64"
owner: linux-kcp
status: done
- id: I5-OAUTH
title: "OAuth sign-in in the app with the requester scope plus chat.atirc.revoke (28); client metadata generated from requester-scope.txt; MarketIdentity Ready in the embedded kcp"
owner: atirc-ios-client-dev
status: done
- id: I6-POLICY-SANDBOX
title: "iOS policy sandbox provider (JavaScriptCore runner + Prober, Fenced condition) so requester-tangled-vouch runs in the app; decision recorded as an ADR"
owner: linux-kcp
status: doing
- id: I7-AGENTS
title: "Agents section: launch an agent VM through the market request flow (EphemeralInfra, ComputeContract with spec.agent), leased token row, Release, revoke; release finished in background time or on next launch"
owner: atirc-ios-client-dev
status: doing
- id: I8-ASK-OWNER
title: "Pending owner questions list, answer sheet and local notification on a new ask (chat.atirc.ask/questions/answer/decide/owner_mode)"
owner: atirc-ios-client-dev
status: done
- id: I9-E2E
title: "End to end on a device: sign in, launch an agent, agent joins the owner channel on its leased token, release, token dead by revoke and by lease; hosts clean"
owner: linux-kcp
status: todo