diff --git a/.trinity/seals/automation_automation::hive_proxy.json b/.trinity/seals/automation_automation::hive_proxy.json new file mode 100644 index 0000000000..4c8fbc13ec --- /dev/null +++ b/.trinity/seals/automation_automation::hive_proxy.json @@ -0,0 +1,28 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+42a4860c4", + "closure": [], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:cb82c5333e96333bb9c5f17bdae70ef2004ab4ee68238d0a7eeb02686297ae80", + "gen_hash_rust": "sha256:516998c1ae410d5ba330645912f3d2f5aed48cb9b39177749d0c0f298383fcd8", + "gen_hash_verilog": "sha256:6bf1743be30fbf51338716b2da167cb89093eef8f263ba9aeacc3f90cebe9196", + "gen_hash_zig": "sha256:fe6d43b73799caedbc8f247941d69e3009c61f0215ca49d1a304deadea5ec0a9", + "module": "automation::hive_proxy", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-20T06:36:58Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:87e194bc577da38e2a6f6f3777a5fc335efe1686e9bb280e57633d55cad79041", + "spec_path": "specs/automation/hive-proxy.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 17, + "total": 17, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:8512b80da533b79289110fca4a65f3336abd7a059ca4b72ea595d5e33ff8ad10", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/.trinity/seals/automation_automation::queen_contributor_keys.json b/.trinity/seals/automation_automation::queen_contributor_keys.json index a888affdab..d72c6da060 100644 --- a/.trinity/seals/automation_automation::queen_contributor_keys.json +++ b/.trinity/seals/automation_automation::queen_contributor_keys.json @@ -1,27 +1,28 @@ { - "built_by": "t27c-bootstrap@0.5.2+5d5d2cdef", + "built_by": "t27c-bootstrap@0.5.2+42a4860c4", "closure": [], "config": "gen=c,rust,verilog,zig;tests=zig", - "gen_hash_c": "sha256:cfc320d6b0312523844cc7775c1a85db57e6cde9057dbaa66399ffc3e74ede20", - "gen_hash_rust": "sha256:17b3d913cc689129448c7444090283d4564b4de2bf553e54d2115a664f7fbd93", - "gen_hash_verilog": "sha256:77554d33b5c5227e53d7079e8d8892523e6d3b40a069e0067ff1b0273becc4d2", - "gen_hash_zig": "sha256:195005a8c2f455b26e279463fd4cac3669f3bd97d916bbac00e8b96eedc9c81d", + "gen_hash_c": "sha256:5f6b990f6f70bf13d5db823016b11808962b299f798b610271021da546ba7685", + "gen_hash_rust": "sha256:3bfa8dd6715547f14d199157a79520f377182a4a901324611a0593f9594b719d", + "gen_hash_verilog": "sha256:bbd16c159a3a302044e58089b2781c8e552104bc64472bc235488e6a35fbc928", + "gen_hash_zig": "sha256:1b048b641504ace0a0e7fff1f2a2800c21af96c820bd701257e3f6e3b13a36b6", "module": "automation::queen_contributor_keys", "ring": 12, "seal_schema": 2, - "sealed_at": "2026-10-09T14:46:42Z", + "sealed_at": "2026-10-20T06:37:02Z", "sealed_by": "t27c-bootstrap@0.5.2", - "spec_hash": "sha256:5cc353dd0ad0b2bd5ee6a92498a3518a02b899e64fb73dfb2f34f67e4322b9a7", + "spec_hash": "sha256:c4bb26241d10100b8e4ab13887307363553eb025ec65ae1f45e0a8d70fb754dc", "spec_path": "specs/automation/queen-contributor-keys.t27", "tests": { "failed": 0, "failing": [], "forced": false, - "passed": 13, - "total": 13 + "passed": 16, + "total": 16, + "vacuous": 0 }, "toolchain": { - "test_runner": "sha256:1982ae948e24d00c5d221ccec12dfce1eb01204b53e37b0f857f93b16d64629d", + "test_runner": "sha256:8512b80da533b79289110fca4a65f3336abd7a059ca4b72ea595d5e33ff8ad10", "zig": "0.16.0" } } \ No newline at end of file diff --git a/specs/automation/hive-proxy.t27 b/specs/automation/hive-proxy.t27 new file mode 100644 index 0000000000..12dd454eac --- /dev/null +++ b/specs/automation/hive-proxy.t27 @@ -0,0 +1,392 @@ +// SPDX-License-Identifier: Apache-2.0 +// specs/automation/hive-proxy.t27 -- the proxy that lets a signed-in person's client spend the +// provider keys that person stored in the hive, over the Anthropic Messages protocol. +// Host: 999-multibots-telegraf apps/vibee-editor/render/src/hive/proxy-route.ts (session, caps, +// the key pick, the stream back) and, in the Queen (gHashTag/trios, trios/agent-server), +// apps/server/src/api/routes/queen-contributor-proxy.ts (opens ONE sealed key for ONE call). +// The Queen's own constants are in queen-contributor-keys.t27 (v3). +// Trace: gHashTag/999-multibots-telegraf#4024; gHashTag/t27#10782 (the T27 OS KEYS tab). +// Generated: t27c gen-ts supplies the constants; scripts/t27-logic.mjs compiles the fn bodies. +// Claim status: decision contract. It says who may spend which key and what a client is told. +// That the provider answers, and how fast, is live evidence this file does not hold. +// Why: owner, 2026-10-20 -- "we had 70 bees working on keys, where are they? connect them all", +// and "set up key orchestration like the tool Free Claude Code": Claude Code, pointed at +// this route, spends the same keys the Queen's bees spend, with the same owner rule. +// WHO. Only a full app session (Authorization: Bearer, the 600 s access token) names an owner, +// telegram:, exactly as /api/hive/keys does. Telegram initData, a delegated agent key, +// a game token and the server key never spend a person's key. A body, a header or a query +// never chooses the owner, a key id or an upstream URL. +// WHICH KEY. Among the owner's keys that are enabled and whose provider speaks the Anthropic +// protocol (zai today), one per call in turn (pick_index). An upstream 429 reaches the client +// unchanged, and the client's own retry lands on the next key: that is the failover, with no +// second pass inside the proxy. A person whose only enabled keys are nvidia (the OpenAI +// protocol, which needs translation, see the provider transform spec) is told so, not guessed at. +// WHICH MODEL. The model of the chosen key, which the owner chose for that provider in the KEYS +// tab ("one model for all keys of a provider", hive-contributor-keys v2). The model a client +// names (claude-sonnet-...) is replaced. No tier table is invented here. +// WHAT THE CLIENT IS TOLD. A provider answer 2xx streams through byte for byte. A provider +// refusal is never echoed: its body can carry a credential. The client gets an +// Anthropic-shaped error whose status and type come from reply_status and reply_type, with +// a stable code. 402, 429 and 529 keep their number. A 401 or 403 means the STORED key is +// refused, not the session, so it reaches the client as 502 api_error and never as +// authentication_error (which would send Claude Code to a login that cannot help). +// THE TOKEN. The 600 s access token is the only credential this route takes. A native client +// renews it through POST /api/auth/refresh as it already does for every other route; a +// separate long-lived proxy token would be a second credential that spends money, so this +// spec does not mint one. +// v1 (2026-10-20): the route, the caps, the pick, the status table. +// phi^2 + 1/phi^2 = 3 | TRINITY + +module automation::hive_proxy { + pub const KIND : str = "automation"; + pub const ID : str = "hive-proxy"; + pub const REPO : str = "999-multibots-telegraf"; + pub const VERSION : u8 = 1; + + pub const ROUTE : str = "/api/hive/proxy/v1/messages"; + pub const FLAG_ENV : str = "HIVE_PROXY"; + pub const REQUEST_MAX_BYTES : u32 = 4194304; + pub const RATE_WINDOW_MS : u32 = 60000; + pub const RATE_MAX_CALLS : u32 = 120; + pub const MAX_IN_FLIGHT : u32 = 8; + pub const BUSY_RETRY_SECONDS : u32 = 10; + pub const RETRY_AFTER_MAX_CHARS : u32 = 6; + pub const KEYS_TIMEOUT_MS : u32 = 10000; + pub const STREAM_TIMEOUT_MS : u32 = 600000; + pub const QUEEN_KEYS_PATH : str = "/queen/contributor-keys/proxy/keys"; + pub const QUEEN_CALL_PREFIX : str = "/queen/contributor-keys/proxy/"; + pub const QUEEN_CALL_SUFFIX : str = "/messages"; + pub const UPSTREAM_HEADER : str = "x-hive-upstream-status"; + pub const CODE_OFF : str = "proxy_disabled"; + pub const CODE_NO_KEY : str = "no_enabled_key"; + pub const CODE_NEEDS_TRANSLATION : str = "provider_needs_translation"; + pub const CODE_TOO_LARGE : str = "request_too_large"; + pub const CODE_RATE : str = "rate_limited"; + pub const CODE_BUSY : str = "too_many_in_flight"; + pub const CODE_QUEEN_DOWN : str = "queen_unavailable"; + pub const CODE_AUTH : str = "authentication_required"; + pub const CODE_BAD_BODY : str = "invalid_body"; + pub const CODE_JSON : str = "json_required"; + pub const CODE_METHOD : str = "method_not_allowed"; + pub const CODE_NOT_CONFIGURED : str = "not_configured"; + + fn flag_on(value: str) -> bool { + return value == "on"; + } + + fn may_spend(via_session: bool, allowed: bool, subject_valid: bool) -> bool { + return via_session && allowed && subject_valid; + } + + fn request_fits(bytes: u32) -> bool { + return bytes > 0 && bytes <= REQUEST_MAX_BYTES; + } + + fn rate_allows(calls_in_window: u32) -> bool { + return calls_in_window < RATE_MAX_CALLS; + } + + fn may_start(in_flight: u32) -> bool { + return in_flight < MAX_IN_FLIGHT; + } + + fn speaks_anthropic(provider: str) -> bool { + return provider == "zai"; + } + + fn key_usable(enabled: bool, provider: str) -> bool { + return enabled && speaks_anthropic(provider); + } + + fn pick_index(usable: u32, turn: u32) -> u32 { + if usable == 0 { + return 0; + } + return turn % usable; + } + + fn no_key_code(any_enabled: bool) -> str { + if any_enabled { + return CODE_NEEDS_TRANSLATION; + } + return CODE_NO_KEY; + } + + fn reply_status(upstream: u16) -> u16 { + if upstream >= 200 && upstream < 300 { + return upstream; + } + if upstream == 400 || upstream == 413 || upstream == 422 { + return upstream; + } + if upstream == 402 || upstream == 429 || upstream == 529 { + return upstream; + } + return 502; + } + + fn reply_type(upstream: u16) -> str { + if upstream == 400 || upstream == 422 { + return "invalid_request_error"; + } + if upstream == 413 { + return "request_too_large"; + } + if upstream == 402 { + return "billing_error"; + } + if upstream == 429 { + return "rate_limit_error"; + } + if upstream == 529 { + return "overloaded_error"; + } + return "api_error"; + } + + fn reply_code(upstream: u16) -> str { + if upstream == 400 || upstream == 422 { + return "upstream_rejected_request"; + } + if upstream == 413 { + return CODE_TOO_LARGE; + } + if upstream == 402 { + return "upstream_payment_required"; + } + if upstream == 429 { + return "upstream_rate_limited"; + } + if upstream == 529 { + return "upstream_overloaded"; + } + if upstream == 401 || upstream == 403 { + return "upstream_key_rejected"; + } + return "upstream_unavailable"; + } + + fn own_type(status: u16) -> str { + if status == 401 { + return "authentication_error"; + } + if status == 404 { + return "not_found_error"; + } + if status == 400 || status == 405 || status == 415 { + return "invalid_request_error"; + } + if status == 413 { + return "request_too_large"; + } + if status == 429 { + return "rate_limit_error"; + } + return "api_error"; + } + + fn queen_status(answered: u16) -> u16 { + if answered == 404 || answered == 413 { + return answered; + } + return 502; + } + + fn queen_code(answered: u16) -> str { + if answered == 404 { + return CODE_NO_KEY; + } + if answered == 413 { + return CODE_TOO_LARGE; + } + return CODE_QUEEN_DOWN; + } + + fn retry_after_ok(chars: u32, digits_only: bool) -> bool { + return chars > 0 && chars <= RETRY_AFTER_MAX_CHARS && digits_only; + } + + fn stream_outlasts(stream_ms: u32, keys_ms: u32) -> bool { + return stream_ms > keys_ms && keys_ms > 0; + } + + fn same_version(declared: u8, noted: u8) -> bool { + return declared == noted; + } + + test "the proxy is off unless the flag says on" { + assert(flag_on("on")); + assert(!flag_on("")); + assert(!flag_on("off")); + assert(!flag_on("true")); + assert(!flag_on("1")); + assert(!flag_on("ON")); + } + + test "only a full app session with a valid subject may spend a key" { + assert(may_spend(true, true, true)); + assert(!may_spend(false, true, true)); + assert(!may_spend(true, false, true)); + assert(!may_spend(true, true, false)); + } + + test "a request is bounded and never empty" { + assert(!request_fits(0)); + assert(request_fits(1)); + assert(request_fits(4194304)); + assert(!request_fits(4194305)); + assert(REQUEST_MAX_BYTES == 4194304); + } + + test "a person has a call rate and a stream count, both strict upper bounds" { + assert(rate_allows(0)); + assert(rate_allows(119)); + assert(!rate_allows(120)); + assert(!rate_allows(121)); + assert(may_start(0)); + assert(may_start(7)); + assert(!may_start(8)); + assert(!may_start(9)); + assert(RATE_WINDOW_MS == 60000); + } + + test "only a provider that speaks the Anthropic protocol can serve a call" { + assert(speaks_anthropic("zai")); + assert(!speaks_anthropic("nvidia")); + assert(!speaks_anthropic("openai")); + assert(!speaks_anthropic("Zai")); + assert(!speaks_anthropic("")); + } + + test "a key is usable when it is enabled and its provider speaks the protocol" { + assert(key_usable(true, "zai")); + assert(!key_usable(false, "zai")); + assert(!key_usable(true, "nvidia")); + assert(!key_usable(false, "nvidia")); + } + + test "the pick walks the usable keys in turn and never leaves the list" { + assert(pick_index(3, 0) == 0); + assert(pick_index(3, 1) == 1); + assert(pick_index(3, 2) == 2); + assert(pick_index(3, 3) == 0); + assert(pick_index(3, 301) == 1); + assert(pick_index(1, 0) == 0); + assert(pick_index(1, 7) == 0); + assert(pick_index(0, 5) == 0); + assert(pick_index(2, 4294967295) == 1); + } + + test "no usable key is told apart from keys that only need translation" { + assert(no_key_code(false) == CODE_NO_KEY); + assert(no_key_code(true) == CODE_NEEDS_TRANSLATION); + assert(CODE_NO_KEY == "no_enabled_key"); + assert(CODE_NEEDS_TRANSLATION == "provider_needs_translation"); + } + + test "an upstream answer reaches the client as the table says" { + assert(reply_status(200) == 200); + assert(reply_status(299) == 299); + assert(reply_status(400) == 400); + assert(reply_status(413) == 413); + assert(reply_status(422) == 422); + assert(reply_status(402) == 402); + assert(reply_status(429) == 429); + assert(reply_status(529) == 529); + assert(reply_status(300) == 502); + assert(reply_status(404) == 502); + assert(reply_status(500) == 502); + assert(reply_status(503) == 502); + assert(reply_status(504) == 502); + assert(reply_status(0) == 502); + } + + test "a refused stored key is never an authentication error to the client" { + assert(reply_status(401) == 502); + assert(reply_status(403) == 502); + assert(reply_type(401) != "authentication_error"); + assert(reply_type(403) != "authentication_error"); + assert(reply_type(401) == "api_error"); + assert(reply_code(401) == "upstream_key_rejected"); + assert(reply_code(403) == "upstream_key_rejected"); + } + + test "every upstream refusal has an Anthropic error type and a stable code" { + assert(reply_type(400) == "invalid_request_error"); + assert(reply_type(422) == "invalid_request_error"); + assert(reply_type(413) == "request_too_large"); + assert(reply_type(402) == "billing_error"); + assert(reply_type(429) == "rate_limit_error"); + assert(reply_type(529) == "overloaded_error"); + assert(reply_type(500) == "api_error"); + assert(reply_code(400) == "upstream_rejected_request"); + assert(reply_code(402) == "upstream_payment_required"); + assert(reply_code(429) == "upstream_rate_limited"); + assert(reply_code(529) == "upstream_overloaded"); + assert(reply_code(500) == "upstream_unavailable"); + assert(reply_code(404) == "upstream_unavailable"); + } + + test "the proxy's own refusals carry an Anthropic error type by their status" { + assert(own_type(401) == "authentication_error"); + assert(own_type(404) == "not_found_error"); + assert(own_type(400) == "invalid_request_error"); + assert(own_type(405) == "invalid_request_error"); + assert(own_type(415) == "invalid_request_error"); + assert(own_type(413) == "request_too_large"); + assert(own_type(429) == "rate_limit_error"); + assert(own_type(502) == "api_error"); + assert(own_type(503) == "api_error"); + assert(CODE_AUTH == "authentication_required"); + assert(CODE_BAD_BODY == "invalid_body"); + assert(CODE_JSON == "json_required"); + assert(CODE_METHOD == "method_not_allowed"); + assert(CODE_NOT_CONFIGURED == "not_configured"); + assert(CODE_OFF == "proxy_disabled"); + assert(CODE_RATE == "rate_limited"); + assert(CODE_BUSY == "too_many_in_flight"); + } + + test "the Queen's own refusals are told by their kind and never as a provider answer" { + assert(queen_status(404) == 404); + assert(queen_status(413) == 413); + assert(queen_status(409) == 502); + assert(queen_status(429) == 502); + assert(queen_status(503) == 502); + assert(queen_code(404) == CODE_NO_KEY); + assert(queen_code(413) == CODE_TOO_LARGE); + assert(queen_code(503) == CODE_QUEEN_DOWN); + assert(queen_code(409) == "queen_unavailable"); + } + + test "a retry-after is a short run of digits" { + assert(retry_after_ok(1, true)); + assert(retry_after_ok(6, true)); + assert(!retry_after_ok(0, true)); + assert(!retry_after_ok(7, true)); + assert(!retry_after_ok(3, false)); + } + + test "the route and the Queen's two doors are fixed paths" { + assert(ROUTE == "/api/hive/proxy/v1/messages"); + assert(QUEEN_KEYS_PATH == "/queen/contributor-keys/proxy/keys"); + assert(QUEEN_CALL_PREFIX == "/queen/contributor-keys/proxy/"); + assert(QUEEN_CALL_SUFFIX == "/messages"); + assert(UPSTREAM_HEADER == "x-hive-upstream-status"); + assert(FLAG_ENV == "HIVE_PROXY"); + } + + test "a stream outlasts a key listing and a key listing is short" { + assert(stream_outlasts(STREAM_TIMEOUT_MS, KEYS_TIMEOUT_MS)); + assert(stream_outlasts(600000, 10000)); + assert(!stream_outlasts(10000, 10000)); + assert(!stream_outlasts(10000, 0)); + assert(stream_outlasts(STREAM_TIMEOUT_MS, BUSY_RETRY_SECONDS)); + } + + test "VERSION says what the header's last note says" { + assert(same_version(VERSION, 1)); + assert(!same_version(VERSION, 2)); + } +} +// phi^2 + 1/phi^2 = 3 | TRINITY diff --git a/specs/automation/queen-contributor-keys.t27 b/specs/automation/queen-contributor-keys.t27 index dd47361961..ef2cbef4f0 100644 --- a/specs/automation/queen-contributor-keys.t27 +++ b/specs/automation/queen-contributor-keys.t27 @@ -10,13 +10,18 @@ // v2 (2026-10-02): one model for all of an owner's keys of a provider, accepted only // after a key of that provider called a tool with it. Probe timeout 15 s -> 90 s: // z-ai/glm-5.3 on NVIDIA answered in 20-46 s and glm-4.5-flash on Z.ai in up to 87 s. +// v3 (2026-10-20): the Queen opens ONE sealed key for ONE call of the Anthropic Messages +// protocol (gHashTag/999-multibots-telegraf#4024; the render side is hive-proxy.t27). The key is +// opened inside the request, handed to the provider and never returned, logged or kept. The +// render attests the owner and picks the key id; the Queen only checks that the key is the +// owner's, is enabled and has an Anthropic endpoint. // phi^2 + 1/phi^2 = 3 | TRINITY module automation::queen_contributor_keys { pub const KIND : str = "automation"; pub const ID : str = "queen-contributor-keys"; pub const REPO : str = "BrowserOS"; - pub const VERSION : u8 = 2; + pub const VERSION : u8 = 3; pub const PROBE_TIMEOUT_MS : u32 = 90000; pub const PROBE_MAX_TOKENS : u16 = 16; @@ -39,6 +44,11 @@ module automation::queen_contributor_keys { pub const MODEL_CHECK_ATTEMPTS : u16 = 2; pub const MODEL_LIST_LIMIT : u16 = 500; pub const MODEL_LIST_CACHE_SECONDS : u16 = 600; + pub const ZAI_ANTHROPIC_URL : str = "https://api.z.ai/api/anthropic"; + pub const PROXY_BODY_MAX_BYTES : u32 = 4259840; + pub const PROXY_TIMEOUT_MS : u32 = 600000; + pub const PROXY_UPSTREAM_HEADER : str = "x-hive-upstream-status"; + pub const PROXY_FLAG_ENV : str = "TRIOS_KEY_PROXY"; fn trusted_proxy(configured_bytes: u32, token_matches: bool, subject_valid: bool) -> bool { return configured_bytes >= PROXY_TOKEN_MIN_BYTES && token_matches && subject_valid; @@ -86,6 +96,26 @@ module automation::queen_contributor_keys { return attempt < MODEL_CHECK_ATTEMPTS; } + fn probe_waits(timeout_ms: u32, answer_ms: u32) -> bool { + return answer_ms < timeout_ms; + } + + fn header_names(version: u8, last_note: u8) -> bool { + return last_note == version; + } + + fn proxy_may_open(enabled: bool, anthropic_native: bool) -> bool { + return enabled && anthropic_native; + } + + fn proxy_flag_on(value: str) -> bool { + return value == "on"; + } + + fn proxy_cap_fits(bytes: u32) -> bool { + return bytes > 0 && bytes <= PROXY_BODY_MAX_BYTES; + } + fn wallet_credit(added: u32) -> u32 { if CREDIT_WALLET { return added; } return 0; @@ -173,10 +203,40 @@ module automation::queen_contributor_keys { test "a probe waits long enough for a slow reasoning model" { assert(PROBE_TIMEOUT_MS >= 60000); assert(MODEL_PROBE_MAX_TOKENS > PROBE_MAX_TOKENS); + assert(probe_waits(PROBE_TIMEOUT_MS, 87000)); + assert(probe_waits(PROBE_TIMEOUT_MS, 46000)); + assert(!probe_waits(PROBE_TIMEOUT_MS, PROBE_TIMEOUT_MS)); + assert(!probe_waits(15000, 20000)); + } + + test "a proxied call opens only an enabled key whose provider has an Anthropic endpoint" { + assert(proxy_may_open(true, true)); + assert(!proxy_may_open(false, true)); + assert(!proxy_may_open(true, false)); + assert(!proxy_may_open(false, false)); + } + + test "the proxied call is off unless the flag says on" { + assert(proxy_flag_on("on")); + assert(!proxy_flag_on("")); + assert(!proxy_flag_on("off")); + assert(!proxy_flag_on("ON")); + assert(PROXY_FLAG_ENV == "TRIOS_KEY_PROXY"); + } + + test "a proxied request is bounded above the render's own cap and never empty" { + assert(!proxy_cap_fits(0)); + assert(proxy_cap_fits(1)); + assert(proxy_cap_fits(4194304)); + assert(proxy_cap_fits(4259840)); + assert(!proxy_cap_fits(4259841)); + assert(PROXY_TIMEOUT_MS > PROBE_TIMEOUT_MS); } test "VERSION says what the header's last note says" { - assert(VERSION == 2); + assert(VERSION == 3); + assert(header_names(VERSION, 3)); + assert(!header_names(VERSION, 2)); } }