Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions .trinity/seals/t27os_T27osPtyLogic.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"built_by": "t27c-bootstrap@0.5.2+e3bb9ffec",
"built_by": "t27c-bootstrap@0.5.2+1417e3bee",
"closure": [
{
"spec": "specs/t27os/agents.t27",
Expand Down Expand Up @@ -27,23 +27,23 @@
}
],
"config": "gen=c,rust,verilog,zig;tests=zig",
"gen_hash_c": "sha256:efb132b84e42963b545ad4950c81cd65a3bb9fd74ece35db758f4af5522e5351",
"gen_hash_rust": "sha256:c5e7da1a403caadc39e1f8a6065e45da5021ebb865d7f42de658aa0d6019ab5e",
"gen_hash_verilog": "sha256:6cd1f9c0f7b5b571e5b1a93641bc68ea44b24634a3652abda34de4ec4c5de93f",
"gen_hash_zig": "sha256:ce84121284bc0007cad586e55c9bfc00b6cb072c3a374a300095ef52476b3aab",
"gen_hash_c": "sha256:ea264f26eee4a971f4fbf7cd9f7de6de72f55cfe82fa9ae98e5ccaa3cfe9f922",
"gen_hash_rust": "sha256:54815c023a8034256634bffc1894fb481027faba8e8915c0c674363efa8877db",
"gen_hash_verilog": "sha256:2ac6b6aa132ffa0bd68f68b896247fb411d29e9d736eb39ab725cd42c563a553",
"gen_hash_zig": "sha256:f5f02c802c02af613dd90592c3e77dd525cc64da594a57c9274f66208a2cbbf3",
"module": "T27osPtyLogic",
"ring": 12,
"seal_schema": 2,
"sealed_at": "2026-10-20T07:50:06Z",
"sealed_at": "2026-10-20T08:04:30Z",
"sealed_by": "t27c-bootstrap@0.5.2",
"spec_hash": "sha256:8b07d4889aa1226c25eda31972d98cde7b8a8271b2f419f1a78c0a320089f4af",
"spec_hash": "sha256:2167ea0959df3a657b97cf7055bf89f622f379b71e73852a3da1d0bacf1e53fa",
"spec_path": "specs/t27os/pty_logic.t27",
"tests": {
"failed": 0,
"failing": [],
"forced": false,
"passed": 34,
"total": 34,
"passed": 37,
"total": 37,
"vacuous": 0
},
"toolchain": {
Expand Down
5 changes: 3 additions & 2 deletions docs/t27os/keys-orchestration.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# T27 OS key orchestration, compared with Free Claude Code

Issue: gHashTag/t27#11224. Code: `specs/t27os/keys.t27` (decisions and data), the host side in
`keys_host.t27`, `pty_server.t27` and `glass_window.t27`. Stage 2 (a local router) is
`keys_host.t27` (the Keychain and the push to the server), `keys_probe.t27` (the check button),
`pty_server.t27` and `glass_window.t27`. Stage 2 (a local router) is
`specs/t27os/router.t27`, decisions only. No key value appears in this file or in any spec; every test uses
fake values.

Expand Down Expand Up @@ -49,7 +50,7 @@ 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 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`) |
| 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`). The PTY server reads the keys itself the same way, at its start and at a spawn that finds no key for the agent (`load_mask_agent`, `keys_reload_due`), so it does not depend on the window being connected; the window's push still adds and overrides |
| 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 |
Expand Down
9 changes: 6 additions & 3 deletions docs/t27os/run-local.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,11 @@ The window does not start the server and does not bundle it. They meet on one Un
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
`<provider>`, then `<provider>#2` .. `#8`. The window and the PTY server reach them only by running
`/usr/bin/security` (`specs/t27os/keys_host.t27`: posix_spawn, pipes, poll, waitpid; the server reads the
Keychain itself at its start and at a spawn that finds no key for the pane's agent, at most once a minute,
so a pane spawned from a script while the window is closed still has its key; the log line is
`keys read from the Keychain: N`, a count, never a key), 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
Expand Down Expand Up @@ -92,7 +95,7 @@ refuses to connect to the socket).
```
# 1. the server first, detached from the tab. PATH must contain the directory of the `claude` CLI.
cd ~ && PATH=$HOME/.local/bin:$PATH <repo>/target/t27os/t27os-pty < /dev/null > <scratch>/pty-server.log 2>&1 &
# the log then reads: t27os-server: listening, restored 0
# the log then reads: t27os-server: keys read from the Keychain: N / t27os-server: listening, restored 0
# 2. the app
open <repo>/target/t27os/T27OS.app
```
Expand Down
2 changes: 1 addition & 1 deletion specs/t27os/glass_window.t27
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ use t27os::keys_host::kc_put_n;
use t27os::keys_host::kc_get_n;
use t27os::keys_host::kc_del_n;
use t27os::keys_host::kc_has;
use t27os::keys_host::probe;
use t27os::keys_probe::probe;
use t27os::keys_host::push_keys;
use t27os::pty_client::client_set_key;
use t27os::editors::EDITOR_COUNT;
Expand Down
102 changes: 8 additions & 94 deletions specs/t27os/keys_host.t27
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
// SPDX-License-Identifier: Apache-2.0
// T27 OS keys_host: where the key manager (keys.t27) touches the machine. Three jobs, no decisions:
// T27 OS keys_host: where the key manager (keys.t27) touches the machine through libSystem. Two jobs, no
// decisions (the check button, which needs Foundation, is keys_probe.t27):
// - the macOS Keychain, one generic-password item per key (up to MAX_KEYS a provider; account = provider
// name, then "<name>#2" .. "<name>#8") under the service "ai.t27.os": kc_get_n / kc_put_n / kc_del_n.
// The Keychain is reached ONLY by running /usr/bin/security (posix_spawn, pipes, poll, waitpid), never
// through SecItem calls. The window is signed ad hoc, so each rebuild is a new code identity and every
// SecItem read of an item asked for the login password again; the items trust /usr/bin/security, which
// Apple signs and no rebuild changes. A key is never written to a file, never put on argv (ps shows it:
// an add feeds `security -i` one line on standard input) and never reaches the repo.
// - the check button: probe() sends the cheapest authenticated request of a provider (a GET; for z.ai one one-token POST) (table: keys.t27) and
// hands back the HTTP status; keys.t27 status_after_probe decides what it means. The key travels in a
// request header, never in the URL.
// - push_keys(): hand every stored key, with its number, to the PTY server (OP_SETKEY) so a pane it spawns
// is logged in and the server can rotate over the keys of a provider.
//
// Two programs import this module: the window (glass_window.t27) and the PTY server (pty_server.t27, which
// reads the Keychain itself with kc_get_n so it never depends on the window being connected). A module's
// @extern imports travel with it, so everything here is libSystem only (posix_spawn, pipe, poll, waitpid): the
// server links against libSystem alone. Nothing may be added here that needs CoreFoundation or Foundation;
// that is why the check button moved to keys_probe.t27.
//
// Specs holding @extern cannot run tests under t27c; every rule these fns follow is a tested fn in keys.t27
// (argument lists, the add line, which keys are safe for it, the exit-status table, the output -> key rule).
// This file only spawns, pipes, reads and waits.
Expand All @@ -27,7 +31,6 @@ module T27osKeysHost;
use t27os::keys::PROVIDER_COUNT;
use t27os::keys::MAX_KEYS;
use t27os::keys::KEYCHAIN_SERVICE;
use t27os::keys::PROBE_TIMEOUT_S;
use t27os::keys::SEC_OP_READ;
use t27os::keys::SEC_OP_WRITE;
use t27os::keys::SEC_OP_DELETE;
Expand All @@ -45,13 +48,6 @@ use t27os::keys::sec_key_from_output;
use t27os::keys::scan_goes_on;
use t27os::keys::keychain_account_n;
use t27os::keys::setkey_byte;
use t27os::keys::probe_url;
use t27os::keys::probe_has;
use t27os::keys::probe_header_name;
use t27os::keys::probe_header_value;
use t27os::keys::probe_post;
use t27os::keys::probe_body;
use t27os::keys::probe_needs_version;
use t27os::pty_proto::get_u64;
use t27os::pty_proto::MAX_FRAME;
use t27os::pty_logic::put_pollfd;
Expand All @@ -66,24 +62,13 @@ use t27os::pty_sys::sys_kill;
use t27os::pty_sys::sys_waitpid;
use t27os::pty_client::client_set_key;

pub const UTF8_ENC : u32 = 134217984;
pub const SIGNAL_PIPE : i32 = 13;
pub const SIGNAL_CHLD : i32 = 20;
pub const HANDLER_DFL : u64 = 0;
pub const HANDLER_IGN : u64 = 1;
pub const SIGKILL_NUMBER : i32 = 9;

fn addr_of(dst: []u8, src: []const u8, n: u64) -> u64 { return @extern("memmove"); }
fn cf_str(alloc: u64, bytes: []const u8, n: usize, enc: u32, ext: bool) -> u64 { return @extern("CFStringCreateWithBytes"); }
fn cf_data(alloc: u64, bytes: []const u8, n: u64) -> u64 { return @extern("CFDataCreate"); }
fn cf_release(o: u64) -> void { @extern("CFRelease"); }
fn class_named(name: u64) -> u64 { return @extern("NSClassFromString"); }
fn sel_named(name: u64) -> u64 { return @extern("NSSelectorFromString"); }
fn msg0(o: u64, s: u64) -> u64 { return @extern("objc_msgSend"); }
fn msg1(o: u64, s: u64, a: u64) -> u64 { return @extern("objc_msgSend"); }
fn msg2(o: u64, s: u64, a: u64, b: u64) -> u64 { return @extern("objc_msgSend"); }
fn msg3(o: u64, s: u64, a: u64, b: u64, c: u64) -> u64 { return @extern("objc_msgSend"); }
fn msg_f(o: u64, s: u64, t: f64) -> u64 { return @extern("objc_msgSend"); }

/// pipe(2): fds[0] is the read end, fds[1] the write end.
fn sys_pipe(fds: [2]u32) -> i32 { return @extern("pipe"); }
Expand All @@ -95,24 +80,6 @@ fn fa_dup2(fa: []u8, fd: i32, to: i32) -> i32 { return @extern("posix_spawn_file
fn fa_close(fa: []u8, fd: i32) -> i32 { return @extern("posix_spawn_file_actions_addclose"); }
fn fa_open(fa: []u8, fd: i32, path: []const u8, flags: i32, mode: u32) -> i32 { return @extern("posix_spawn_file_actions_addopen"); }

fn cf(s: []const u8) -> u64 {
return cf_str(0, s, s.len, UTF8_ENC, false);
}

fn cls(s: []const u8) -> u64 {
const n : u64 = cf(s);
const c : u64 = class_named(n);
cf_release(n);
return c;
}

fn sel(s: []const u8) -> u64 {
const n : u64 = cf(s);
const c : u64 = sel_named(n);
cf_release(n);
return c;
}

fn close_fd(fd: u32) -> void {
sys_close(@as(i32, @intCast(fd)));
}
Expand Down Expand Up @@ -369,59 +336,6 @@ pub fn kc_has(p: u32) -> bool {
return n > 0;
}

fn add_header(req: u64, name: []const u8, value: []const u8) -> void {
const n : u64 = cf(name);
const v : u64 = cf(value);
msg2(req, sel("setValue:forHTTPHeaderField:"), v, n);
cf_release(n);
cf_release(v);
}

/// The check button: one authenticated GET for provider p with `key`; the HTTP status, 0 when nothing
/// answered (no network, timeout) and 1 when the provider has no probe. Blocks up to PROBE_TIMEOUT_S.
pub fn probe(p: u32, key: []const u8) -> u32 {
if (probe_has(p) == false) { return 1; }
var ub : [160]u8 = undefined;
const un : usize = probe_url(p, ub[0..160]);
const us : u64 = cf(ub[0..un]);
const url : u64 = msg1(cls("NSURL"), sel("URLWithString:"), us);
cf_release(us);
if (url == 0) { return 0; }
const req : u64 = msg1(cls("NSMutableURLRequest"), sel("requestWithURL:"), url);
if (req == 0) { return 0; }
msg_f(req, sel("setTimeoutInterval:"), @as(f64, @floatFromInt(PROBE_TIMEOUT_S)));
var hn : [24]u8 = undefined;
var hv : [300]u8 = undefined;
const nl : usize = probe_header_name(p, hn[0..24]);
const vl : usize = probe_header_value(p, key, hv[0..300]);
add_header(req, hn[0..nl], hv[0..vl]);
if (probe_needs_version(p)) { add_header(req, "anthropic-version", "2023-06-01"); }
if (probe_post(p)) {
var bb : [200]u8 = undefined;
const bl : usize = probe_body(p, bb[0..200]);
const data : u64 = cf_data(0, bb[0..bl], @as(u64, bl));
const post : u64 = cf("POST");
msg1(req, sel("setHTTPMethod:"), post);
msg1(req, sel("setHTTPBody:"), data);
add_header(req, "content-type", "application/json");
cf_release(post);
cf_release(data);
}
add_header(req, "User-Agent", "t27os");
var resp : [8]u8 = undefined;
var err : [8]u8 = undefined;
var i : usize = 0;
while (i < 8) {
resp[i] = 0;
err[i] = 0;
i = i + 1;
}
msg3(cls("NSURLConnection"), sel("sendSynchronousRequest:returningResponse:error:"), req, addr_of(resp[0..8], resp[0..8], 0), addr_of(err[0..8], err[0..8], 0));
const r : u64 = get_u64(resp[0..8], 0);
if (r == 0) { return 0; }
return @as(u32, @intCast(msg0(r, sel("statusCode"))));
}

/// Hand every stored key to the PTY server on `fd`, each with its number; the count pushed. The numbered
/// accounts of every provider are read (a scan stops after two misses in a row, keys.t27 scan_goes_on) and the
/// number rides in the OP_SETKEY pane byte (keys.t27 setkey_byte), so the server holds all of them and
Expand Down
117 changes: 117 additions & 0 deletions specs/t27os/keys_probe.t27
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// SPDX-License-Identifier: Apache-2.0
// T27 OS keys_probe: the check button of the KEYS tab -- the one host call of the key manager that needs
// Foundation. probe() sends the cheapest authenticated request of a provider (a GET; for z.ai one one-token
// POST) (table: keys.t27) and hands back the HTTP status; keys.t27 status_after_probe decides what it means.
// The key travels in a request header, never in the URL.
//
// It lives apart from keys_host.t27 (the Keychain and the push to the PTY server, libSystem only) because a
// module's @extern imports travel with it: the PTY server imports keys_host to read the Keychain itself, and a
// server that linked CoreFoundation and the Objective-C runtime for a probe it never sends would not link
// against libSystem alone. Only the window imports this module.
//
// Specs holding @extern cannot run tests under t27c; every rule probe follows is a tested fn in keys.t27
// (url, header name and value, body, whether a probe exists). This file only builds the request and sends it.
//
// Refs gHashTag/t27#10782, gHashTag/t27#11241.
module T27osKeysProbe;

use t27os::keys::PROBE_TIMEOUT_S;
use t27os::keys::probe_url;
use t27os::keys::probe_has;
use t27os::keys::probe_header_name;
use t27os::keys::probe_header_value;
use t27os::keys::probe_post;
use t27os::keys::probe_body;
use t27os::keys::probe_needs_version;
use t27os::pty_proto::get_u64;
use t27os::pty_sys::sys_addr;

pub const UTF8_ENC : u32 = 134217984;

fn cf_str(alloc: u64, bytes: []const u8, n: usize, enc: u32, ext: bool) -> u64 { return @extern("CFStringCreateWithBytes"); }
fn cf_data(alloc: u64, bytes: []const u8, n: u64) -> u64 { return @extern("CFDataCreate"); }
fn cf_release(o: u64) -> void { @extern("CFRelease"); }
fn class_named(name: u64) -> u64 { return @extern("NSClassFromString"); }
fn sel_named(name: u64) -> u64 { return @extern("NSSelectorFromString"); }
fn msg0(o: u64, s: u64) -> u64 { return @extern("objc_msgSend"); }
fn msg1(o: u64, s: u64, a: u64) -> u64 { return @extern("objc_msgSend"); }
fn msg2(o: u64, s: u64, a: u64, b: u64) -> u64 { return @extern("objc_msgSend"); }
fn msg3(o: u64, s: u64, a: u64, b: u64, c: u64) -> u64 { return @extern("objc_msgSend"); }
fn msg_f(o: u64, s: u64, t: f64) -> u64 { return @extern("objc_msgSend"); }

fn cf(s: []const u8) -> u64 {
return cf_str(0, s, s.len, UTF8_ENC, false);
}

fn cls(s: []const u8) -> u64 {
const n : u64 = cf(s);
const c : u64 = class_named(n);
cf_release(n);
return c;
}

fn sel(s: []const u8) -> u64 {
const n : u64 = cf(s);
const c : u64 = sel_named(n);
cf_release(n);
return c;
}

fn add_header(req: u64, name: []const u8, value: []const u8) -> void {
const n : u64 = cf(name);
const v : u64 = cf(value);
msg2(req, sel("setValue:forHTTPHeaderField:"), v, n);
cf_release(n);
cf_release(v);
}

/// The check button: one authenticated GET for provider p with `key`; the HTTP status, 0 when nothing
/// answered (no network, timeout) and 1 when the provider has no probe. Blocks up to PROBE_TIMEOUT_S.
pub fn probe(p: u32, key: []const u8) -> u32 {
if (probe_has(p) == false) { return 1; }
var ub : [160]u8 = undefined;
const un : usize = probe_url(p, ub[0..160]);
const us : u64 = cf(ub[0..un]);
const url : u64 = msg1(cls("NSURL"), sel("URLWithString:"), us);
cf_release(us);
if (url == 0) { return 0; }
const req : u64 = msg1(cls("NSMutableURLRequest"), sel("requestWithURL:"), url);
if (req == 0) { return 0; }
msg_f(req, sel("setTimeoutInterval:"), @as(f64, @floatFromInt(PROBE_TIMEOUT_S)));
var hn : [24]u8 = undefined;
var hv : [300]u8 = undefined;
const nl : usize = probe_header_name(p, hn[0..24]);
const vl : usize = probe_header_value(p, key, hv[0..300]);
add_header(req, hn[0..nl], hv[0..vl]);
if (probe_needs_version(p)) { add_header(req, "anthropic-version", "2023-06-01"); }
if (probe_post(p)) {
var bb : [200]u8 = undefined;
const bl : usize = probe_body(p, bb[0..200]);
const data : u64 = cf_data(0, bb[0..bl], @as(u64, bl));
const post : u64 = cf("POST");
msg1(req, sel("setHTTPMethod:"), post);
msg1(req, sel("setHTTPBody:"), data);
add_header(req, "content-type", "application/json");
cf_release(post);
cf_release(data);
}
add_header(req, "User-Agent", "t27os");
var resp : [8]u8 = undefined;
var err : [8]u8 = undefined;
var i : usize = 0;
while (i < 8) {
resp[i] = 0;
err[i] = 0;
i = i + 1;
}
msg3(cls("NSURLConnection"), sel("sendSynchronousRequest:returningResponse:error:"), req, sys_addr(resp[0..8], resp[0..8], 0), sys_addr(err[0..8], err[0..8], 0));
const r : u64 = get_u64(resp[0..8], 0);
if (r == 0) { return 0; }
return @as(u32, @intCast(msg0(r, sel("statusCode"))));
}

test the_probe_module_is_declared {
var b : [4]u8 = undefined;
b[0] = 1;
assert b[0] == 1;
}
Loading
Loading