~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
146 lines · 11.0 KB Raw History
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