Skip to content
22 changes: 13 additions & 9 deletions .trinity/seals/t27os_T27osKeys.json
Original file line number Diff line number Diff line change
@@ -1,33 +1,37 @@
{
"built_by": "t27c-bootstrap@0.5.2+ef2e7ee45",
"built_by": "t27c-bootstrap@0.5.2+e3bb9ffec",
"closure": [
{
"spec": "specs/t27os/agents.t27",
"spec_hash": "sha256:c4b89c1bd1dcb751fada2ae1f0330e250d17c5085ea73acdc38fac13548277ee"
},
{
"spec": "specs/t27os/editors.t27",
"spec_hash": "sha256:fc3608c5131f7fae21d6084102a019fc0425117ecf7544ec1eebfa8155d74f72"
},
{
"spec": "specs/t27os/session_store.t27",
"spec_hash": "sha256:7d05d7dda14587458a77291c3d587e3f08516d8e678f0a4bd06dcaab7f6d1f2f"
}
],
"config": "gen=c,rust,verilog,zig;tests=zig",
"gen_hash_c": "sha256:1c51eed7ba8145e217666faa01f365f1742cdeeb71517ffabc8239d1cd08aff3",
"gen_hash_rust": "sha256:65387d614d4714c7dff0227fb378c33bc7886c695cc42f4ea24458223de573c8",
"gen_hash_verilog": "sha256:9e29c63b98f00cd92e6b0466d356a9d3911bf444f26a652d5e2e6e6b576c6ade",
"gen_hash_zig": "sha256:d244fcdfb0f95fcf8a2742598dbc84d7e66c6c3aec8b4bc632483753564f8230",
"gen_hash_c": "sha256:4da05e755130405b7f85bda4fc5d3456c3638bf567211955a00c2b35fb3448af",
"gen_hash_rust": "sha256:51146ffb9d4d9e2fefc266395eb6ed161971ab4dc9ca949b0650cd008c19e9ce",
"gen_hash_verilog": "sha256:2f1f76f240eb66cd9bee6e5718325f36892e3721ec4d848a670a50c034a484de",
"gen_hash_zig": "sha256:b6aa8439522547b063b1468735ade4eeda04afb2f5624f8a27e59857b8030867",
"module": "T27osKeys",
"ring": 12,
"seal_schema": 2,
"sealed_at": "2026-10-20T07:11:32Z",
"sealed_at": "2026-10-20T07:50:02Z",
"sealed_by": "t27c-bootstrap@0.5.2",
"spec_hash": "sha256:bc12487689ca25d46a0155a6cbb30e36983c4898a7e28486266d084c570f8d43",
"spec_hash": "sha256:35efa3efc95753da265df018e8931d7de5366081a54649e153e074dfd1bae697",
"spec_path": "specs/t27os/keys.t27",
"tests": {
"failed": 0,
"failing": [],
"forced": false,
"passed": 63,
"total": 63,
"passed": 81,
"total": 81,
"vacuous": 0
},
"toolchain": {
Expand Down
18 changes: 9 additions & 9 deletions .trinity/seals/t27os_T27osPtyLogic.json
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
{
"built_by": "t27c-bootstrap@0.5.2+ef2e7ee45",
"built_by": "t27c-bootstrap@0.5.2+e3bb9ffec",
"closure": [
{
"spec": "specs/t27os/agents.t27",
"spec_hash": "sha256:c4b89c1bd1dcb751fada2ae1f0330e250d17c5085ea73acdc38fac13548277ee"
},
{
"spec": "specs/t27os/editors.t27",
"spec_hash": "sha256:9b4e3c083e7aa0d3d3b42ac77f6349820e123d34617abbdfc85358cd22420ede"
"spec_hash": "sha256:fc3608c5131f7fae21d6084102a019fc0425117ecf7544ec1eebfa8155d74f72"
},
{
"spec": "specs/t27os/keys.t27",
"spec_hash": "sha256:bc12487689ca25d46a0155a6cbb30e36983c4898a7e28486266d084c570f8d43"
"spec_hash": "sha256:35efa3efc95753da265df018e8931d7de5366081a54649e153e074dfd1bae697"
},
{
"spec": "specs/t27os/pane_state.t27",
Expand All @@ -27,16 +27,16 @@
}
],
"config": "gen=c,rust,verilog,zig;tests=zig",
"gen_hash_c": "sha256:2cd657507566102e43169919e53ce6aa267cba35b7315127d98321a1f1143911",
"gen_hash_rust": "sha256:b1f9544872abbca303b2b4565a357e4134da53bf232b7efcf2d729d13c3a987a",
"gen_hash_verilog": "sha256:9c28b828e7de42515be178c72ec47fea0b3a20cb6840e9c1a4fbf8085862e08d",
"gen_hash_zig": "sha256:39ac7d141b3d9832cc4ec5e92e9f73e2cf030c4379c4f3253696d46f159efb62",
"gen_hash_c": "sha256:efb132b84e42963b545ad4950c81cd65a3bb9fd74ece35db758f4af5522e5351",
"gen_hash_rust": "sha256:c5e7da1a403caadc39e1f8a6065e45da5021ebb865d7f42de658aa0d6019ab5e",
"gen_hash_verilog": "sha256:6cd1f9c0f7b5b571e5b1a93641bc68ea44b24634a3652abda34de4ec4c5de93f",
"gen_hash_zig": "sha256:ce84121284bc0007cad586e55c9bfc00b6cb072c3a374a300095ef52476b3aab",
"module": "T27osPtyLogic",
"ring": 12,
"seal_schema": 2,
"sealed_at": "2026-10-20T07:11:37Z",
"sealed_at": "2026-10-20T07:50:06Z",
"sealed_by": "t27c-bootstrap@0.5.2",
"spec_hash": "sha256:401e66c84055044c98546c093cbdb677ae5cb5203d524b01e172b37a7afc1e33",
"spec_hash": "sha256:8b07d4889aa1226c25eda31972d98cde7b8a8271b2f419f1a78c0a320089f4af",
"spec_path": "specs/t27os/pty_logic.t27",
"tests": {
"failed": 0,
Expand Down
4 changes: 2 additions & 2 deletions docs/t27os/keys-orchestration.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,13 @@ No proxy is involved.
| Per-tier overrides (opus, sonnet, haiku) | Yes, as data, one column per tier; an empty field falls back to the default; the small-fast variable follows haiku | `MODELS_OPUS/SONNET/HAIKU`, `tier_model`, `profile_var_name/text` | A fourth tier (the one the OpenRouter and Kimi pages call fable) is not mapped; changing a tier is a data edit, not a screen |
| `provider/model` syntax | A profile is a provider; its models sit in the tier table | `PROFILE_NAMES`, `PROFILE_PROVIDER` | The combined one-line syntax; it exists in Free Claude Code because its proxy picks the upstream per request |
| Many providers | Four profiles today: z.ai, Anthropic direct, OpenRouter, Ollama cloud, over the nine key rows | `PROFILE_PROVIDER`, `PROFILE_BASE_URLS`, `PROFILE_KEY_VARS` | A provider that does not speak the Anthropic protocol needs the local router and `specs/provider/transform.t27` (#10902). DeepSeek, Moonshot and MiniMax speak it but need key rows of their own (see below) |
| Numbered keys, key 1 first | Naming and choice are decided: key 1 keeps the existing Keychain item, keys 2 and 3 add `#2`, `#3`; least-recently-used rotation with a rest time | `keychain_label_n`, `MAX_KEYS`, `rotate_pick`, `rotate_pick_except` | The KEYS tab still shows one row per provider and the host reads only key 1; storing and listing keys 2 and 3 is host work |
| Numbered keys, key 1 first | Naming and choice are decided: key 1 keeps the existing Keychain item, keys 2 to 8 add `#2` to `#8`; least-recently-used rotation with a rest time | `keychain_label_n`, `MAX_KEYS`, `rotate_pick`, `rotate_pick_except` | Built: the host reads every numbered key, up to 8 per provider (`MAX_KEYS`), only through `/usr/bin/security`, never through SecItem; the KEYS tab adds the next free number, deletes the newest key and checks every key (`next_free_key`, `newest_key`, `provider_status`) |
| A fallback entry names a key number | Decided: a throttled key (429) with a spare key swaps the key and keeps the profile | `fo_action` = `FA_RESPAWN_KEY`, `fo_target` | The host does not pass the key number to the respawn yet |
| Ordered fallback list after retries | Yes, per pane: the profile list is walked forward only | `next_profile`, `fo_action` = `FA_RESPAWN_NEXT`, `MAX_FAILOVERS` | Per request, inside one conversation turn: needs the local router. A respawn does not replay the request that failed; the person continues in the resumed session |
| Retries before the fallback | Not ours: Claude Code retries by itself, and the failover starts when the error text reaches the screen | `classify_line` | Nothing to build |
| Reasoning effort per route | No | none | Out of scope; not set by any profile |
| Bearer token for the local proxy | Not needed, there is no proxy | none | Needed with the router (it binds 127.0.0.1, `ROUTER_BIND`) |
| Admin page for keys | Yes: the KEYS tab, Keychain storage, masked display, a check button, a circuit breaker | `ky_*`, `mask_key`, `status_after_probe`, `circuit_fails`, `bad_mask` | The signed-in account keys on app.t27.ai cannot be spent from a pane: no proxy route exists server-side (see the MAP block in `keys.t27`) |
| Admin page for keys | Yes: the KEYS tab, Keychain storage through `/usr/bin/security`, masked display of the newest key, a check button, a circuit breaker | `ky_*`, `mask_key`, `status_after_probe`, `circuit_fails`, `bad_mask` | The signed-in account keys on app.t27.ai cannot be spent from a pane: no proxy route exists server-side (see the MAP block in `keys.t27`) |
| Local models (LM Studio, llama.cpp, Ollama) | Ollama cloud only | profile `ollama-cloud` | Local Ollama needs no key, so the stored-keys mask cannot say it is available; a host fact (is the server up) must join the mask first |
| OAuth sign-in for some providers | No | none | Out of scope |

Expand Down
13 changes: 13 additions & 0 deletions docs/t27os/run-local.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,19 @@ The window does not start the server and does not bundle it. They meet on one Un
`glass_window.t27`), the server from `$HOME` (`socket_path` in `pty_proto.t27`). With no server the Claude Code
pane in the window says "offline" and echoes keys.

Provider keys (the KEYS tab, up to 8 per provider) live in the macOS Keychain, service `ai.t27.os`, account
`<provider>`, then `<provider>#2` .. `#8`. The window reaches them only by running `/usr/bin/security`
(`specs/t27os/keys_host.t27`: posix_spawn, pipes, poll, waitpid), never through the SecItem calls, so
**Security.framework is not linked** and the link line in section 3 is complete (measured 2026-10-20: the
SecItem build did not link without `-framework Security`; this one does). The reason: the window is signed
ad hoc, every rebuild is a new code identity to macOS, and each SecItem read asked for the login password
again. `/usr/bin/security` is signed by Apple and never changes, so a read of an item it created shows no
dialog across rebuilds. A key is never put on argv (`ps` shows it): an add writes one line to the stdin of
`security -i`, and refuses a key with white space, a quote or a backslash. An add never replaces a key
(the account must be free); a delete removes the newest key only. Items that an older SecItem build made
trust that build, not `security`, so a read of one may still ask once: remove it in Keychain Access and add
the key again in the KEYS tab.

## 2. Prerequisites

- Apple silicon, Xcode installed and selected (`xcode-select -p`): `ld`, the SDK and `codesign`. Command Line
Expand Down
Loading
Loading