diff --git a/gateway/auth/fixtures/01-simple.json b/gateway/auth/fixtures/01-simple.json index 8afc4c184..1d9282578 100644 --- a/gateway/auth/fixtures/01-simple.json +++ b/gateway/auth/fixtures/01-simple.json @@ -1,5 +1,5 @@ { - "description": "single-key org, custodial phase 1: one user_authorization, one invocation, zero attenuations", + "description": "single-key org, custodial phase 1: one user_authorization, one invocation, zero attenuations. Single-swarm shape — no realm fields anywhere.", "inputs": { "org_id": "org_acme", "org_priv_hex": "0000000000000000000000000000000000000000000000000000000000000001", @@ -17,24 +17,17 @@ "alg": "ed25519", "key": "d04ab232742bb4ab3a1368bd4615e4e6d0224ab71a016baf8520a332c9778737" }, - "permissions": { - "realms": [ - "w1", - "w2" - ], - "agents": [ - "coder", - "browser", - "web-search", - "repair-agent" - ] - }, + "agents": [ + "coder", + "browser", + "web-search", + "repair-agent" + ], "iat": "2026-05-14T09:00:00Z", "exp": "2026-06-14T09:00:00Z", "nonce": "9f4e1c8b2a3d4e5f6a7b8c9d0e1f2a3b" }, "inv_unsigned": { - "realm": "w1", "agents": [ "coder" ], @@ -48,17 +41,16 @@ "atts_unsigned": [] }, "expected": { - "ua_signing_bytes_hex": "7b22657870223a22323032362d30362d31345430393a30303a30305a222c22696174223a22323032362d30352d31345430393a30303a30305a222c226e6f6e6365223a223966346531633862326133643465356636613762386339643065316632613362222c227065726d697373696f6e73223a7b226167656e7473223a5b22636f646572222c2262726f77736572222c227765622d736561726368222c227265706169722d6167656e74225d2c227265616c6d73223a5b227731222c227732225d7d2c22757365725f6964223a22755f616c696365222c22757365725f7075626b6579223a7b22616c67223a2265643235353139222c226b6579223a2264303461623233323734326262346162336131333638626434363135653465366430323234616237316130313662616638353230613333326339373738373337227d7d", - "inv_signing_bytes_hex": "7b226167656e7473223a5b22636f646572225d2c22657870223a22323032362d30352d31345431303a31303a30305a222c22696174223a22323032362d30352d31345431303a30303a30305a222c226d61785f636f73745f757364223a352c226d61785f7374657073223a3130302c226e6f6e6365223a223763326133623463356436653766386139623063316432653366346135623663222c227265616c6d223a227731222c2272756e5f6964223a22725f30316838616c706861726f6f74696e766f636174696f6e3030227d", - "ua_canonical_json": "{\"exp\":\"2026-06-14T09:00:00Z\",\"iat\":\"2026-05-14T09:00:00Z\",\"nonce\":\"9f4e1c8b2a3d4e5f6a7b8c9d0e1f2a3b\",\"permissions\":{\"agents\":[\"coder\",\"browser\",\"web-search\",\"repair-agent\"],\"realms\":[\"w1\",\"w2\"]},\"user_id\":\"u_alice\",\"user_pubkey\":{\"alg\":\"ed25519\",\"key\":\"d04ab232742bb4ab3a1368bd4615e4e6d0224ab71a016baf8520a332c9778737\"}}", - "inv_canonical_json": "{\"agents\":[\"coder\"],\"exp\":\"2026-05-14T10:10:00Z\",\"iat\":\"2026-05-14T10:00:00Z\",\"max_cost_usd\":5,\"max_steps\":100,\"nonce\":\"7c2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c\",\"realm\":\"w1\",\"run_id\":\"r_01h8alpharootinvocation00\"}", - "macaroon_canonical_json": "{\"attenuations\":[],\"invocation\":{\"agents\":[\"coder\"],\"exp\":\"2026-05-14T10:10:00Z\",\"iat\":\"2026-05-14T10:00:00Z\",\"max_cost_usd\":5,\"max_steps\":100,\"nonce\":\"7c2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c\",\"realm\":\"w1\",\"run_id\":\"r_01h8alpharootinvocation00\",\"user_sig\":{\"alg\":\"ed25519\",\"sig\":\"9835547aa1f05a02b129e3a8f39a1389d2bee62b15c8996f6d9776bab6f68853830042e118f7cf1bcfc087e57d8c48115325d1370e3f494bd02b558c76d97202\"}},\"org_id\":\"org_acme\",\"user_authorization\":{\"exp\":\"2026-06-14T09:00:00Z\",\"iat\":\"2026-05-14T09:00:00Z\",\"nonce\":\"9f4e1c8b2a3d4e5f6a7b8c9d0e1f2a3b\",\"org_sig\":{\"alg\":\"ecdsa-secp256k1-sha256\",\"sig\":\"8f1dc45d5fb83ea7f4f06e11ba2a7540f2ecbca0aa8c0c0b357b24c35ff4c70017b23592b91b174f2c0540312d94a4b90e8a6477518a88436039405bcdcd4e5b\"},\"permissions\":{\"agents\":[\"coder\",\"browser\",\"web-search\",\"repair-agent\"],\"realms\":[\"w1\",\"w2\"]},\"user_id\":\"u_alice\",\"user_pubkey\":{\"alg\":\"ed25519\",\"key\":\"d04ab232742bb4ab3a1368bd4615e4e6d0224ab71a016baf8520a332c9778737\"}},\"v\":1}", - "macaroon_b64url": "eyJhdHRlbnVhdGlvbnMiOltdLCJpbnZvY2F0aW9uIjp7ImFnZW50cyI6WyJjb2RlciJdLCJleHAiOiIyMDI2LTA1LTE0VDEwOjEwOjAwWiIsImlhdCI6IjIwMjYtMDUtMTRUMTA6MDA6MDBaIiwibWF4X2Nvc3RfdXNkIjo1LCJtYXhfc3RlcHMiOjEwMCwibm9uY2UiOiI3YzJhM2I0YzVkNmU3ZjhhOWIwYzFkMmUzZjRhNWI2YyIsInJlYWxtIjoidzEiLCJydW5faWQiOiJyXzAxaDhhbHBoYXJvb3RpbnZvY2F0aW9uMDAiLCJ1c2VyX3NpZyI6eyJhbGciOiJlZDI1NTE5Iiwic2lnIjoiOTgzNTU0N2FhMWYwNWEwMmIxMjllM2E4ZjM5YTEzODlkMmJlZTYyYjE1Yzg5OTZmNmQ5Nzc2YmFiNmY2ODg1MzgzMDA0MmUxMThmN2NmMWJjZmMwODdlNTdkOGM0ODExNTMyNWQxMzcwZTNmNDk0YmQwMmI1NThjNzZkOTcyMDIifX0sIm9yZ19pZCI6Im9yZ19hY21lIiwidXNlcl9hdXRob3JpemF0aW9uIjp7ImV4cCI6IjIwMjYtMDYtMTRUMDk6MDA6MDBaIiwiaWF0IjoiMjAyNi0wNS0xNFQwOTowMDowMFoiLCJub25jZSI6IjlmNGUxYzhiMmEzZDRlNWY2YTdiOGM5ZDBlMWYyYTNiIiwib3JnX3NpZyI6eyJhbGciOiJlY2RzYS1zZWNwMjU2azEtc2hhMjU2Iiwic2lnIjoiOGYxZGM0NWQ1ZmI4M2VhN2Y0ZjA2ZTExYmEyYTc1NDBmMmVjYmNhMGFhOGMwYzBiMzU3YjI0YzM1ZmY0YzcwMDE3YjIzNTkyYjkxYjE3NGYyYzA1NDAzMTJkOTRhNGI5MGU4YTY0Nzc1MThhODg0MzYwMzk0MDViY2RjZDRlNWIifSwicGVybWlzc2lvbnMiOnsiYWdlbnRzIjpbImNvZGVyIiwiYnJvd3NlciIsIndlYi1zZWFyY2giLCJyZXBhaXItYWdlbnQiXSwicmVhbG1zIjpbIncxIiwidzIiXX0sInVzZXJfaWQiOiJ1X2FsaWNlIiwidXNlcl9wdWJrZXkiOnsiYWxnIjoiZWQyNTUxOSIsImtleSI6ImQwNGFiMjMyNzQyYmI0YWIzYTEzNjhiZDQ2MTVlNGU2ZDAyMjRhYjcxYTAxNmJhZjg1MjBhMzMyYzk3Nzg3MzcifX0sInYiOjF9", + "ua_signing_bytes_hex": "7b226167656e7473223a5b22636f646572222c2262726f77736572222c227765622d736561726368222c227265706169722d6167656e74225d2c22657870223a22323032362d30362d31345430393a30303a30305a222c22696174223a22323032362d30352d31345430393a30303a30305a222c226e6f6e6365223a223966346531633862326133643465356636613762386339643065316632613362222c22757365725f6964223a22755f616c696365222c22757365725f7075626b6579223a7b22616c67223a2265643235353139222c226b6579223a2264303461623233323734326262346162336131333638626434363135653465366430323234616237316130313662616638353230613333326339373738373337227d7d", + "inv_signing_bytes_hex": "7b226167656e7473223a5b22636f646572225d2c22657870223a22323032362d30352d31345431303a31303a30305a222c22696174223a22323032362d30352d31345431303a30303a30305a222c226d61785f636f73745f757364223a352c226d61785f7374657073223a3130302c226e6f6e6365223a223763326133623463356436653766386139623063316432653366346135623663222c2272756e5f6964223a22725f30316838616c706861726f6f74696e766f636174696f6e3030227d", + "ua_canonical_json": "{\"agents\":[\"coder\",\"browser\",\"web-search\",\"repair-agent\"],\"exp\":\"2026-06-14T09:00:00Z\",\"iat\":\"2026-05-14T09:00:00Z\",\"nonce\":\"9f4e1c8b2a3d4e5f6a7b8c9d0e1f2a3b\",\"user_id\":\"u_alice\",\"user_pubkey\":{\"alg\":\"ed25519\",\"key\":\"d04ab232742bb4ab3a1368bd4615e4e6d0224ab71a016baf8520a332c9778737\"}}", + "inv_canonical_json": "{\"agents\":[\"coder\"],\"exp\":\"2026-05-14T10:10:00Z\",\"iat\":\"2026-05-14T10:00:00Z\",\"max_cost_usd\":5,\"max_steps\":100,\"nonce\":\"7c2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c\",\"run_id\":\"r_01h8alpharootinvocation00\"}", + "macaroon_canonical_json": "{\"attenuations\":[],\"invocation\":{\"agents\":[\"coder\"],\"exp\":\"2026-05-14T10:10:00Z\",\"iat\":\"2026-05-14T10:00:00Z\",\"max_cost_usd\":5,\"max_steps\":100,\"nonce\":\"7c2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c\",\"run_id\":\"r_01h8alpharootinvocation00\",\"user_sig\":{\"alg\":\"ed25519\",\"sig\":\"c82c8f3ec3ac0ef87d5e28a47c578d33729ba630091bbdc767bd75f55e7af84bb30f9c4bce363205ef79e513caca60758a8f1e02d8d520225a6e317a5092560c\"}},\"org_id\":\"org_acme\",\"user_authorization\":{\"agents\":[\"coder\",\"browser\",\"web-search\",\"repair-agent\"],\"exp\":\"2026-06-14T09:00:00Z\",\"iat\":\"2026-05-14T09:00:00Z\",\"nonce\":\"9f4e1c8b2a3d4e5f6a7b8c9d0e1f2a3b\",\"org_sig\":{\"alg\":\"ecdsa-secp256k1-sha256\",\"sig\":\"c81da96f9881db1fa86f04ca40c403d33d0e0020c5c58e22cb7f27a695aec6e650d9383af1101ac13338f867905693f4129861e88d69d07cbcb986662638d8c9\"},\"user_id\":\"u_alice\",\"user_pubkey\":{\"alg\":\"ed25519\",\"key\":\"d04ab232742bb4ab3a1368bd4615e4e6d0224ab71a016baf8520a332c9778737\"}},\"v\":1}", + "macaroon_b64url": "eyJhdHRlbnVhdGlvbnMiOltdLCJpbnZvY2F0aW9uIjp7ImFnZW50cyI6WyJjb2RlciJdLCJleHAiOiIyMDI2LTA1LTE0VDEwOjEwOjAwWiIsImlhdCI6IjIwMjYtMDUtMTRUMTA6MDA6MDBaIiwibWF4X2Nvc3RfdXNkIjo1LCJtYXhfc3RlcHMiOjEwMCwibm9uY2UiOiI3YzJhM2I0YzVkNmU3ZjhhOWIwYzFkMmUzZjRhNWI2YyIsInJ1bl9pZCI6InJfMDFoOGFscGhhcm9vdGludm9jYXRpb24wMCIsInVzZXJfc2lnIjp7ImFsZyI6ImVkMjU1MTkiLCJzaWciOiJjODJjOGYzZWMzYWMwZWY4N2Q1ZTI4YTQ3YzU3OGQzMzcyOWJhNjMwMDkxYmJkYzc2N2JkNzVmNTVlN2FmODRiYjMwZjljNGJjZTM2MzIwNWVmNzllNTEzY2FjYTYwNzU4YThmMWUwMmQ4ZDUyMDIyNWE2ZTMxN2E1MDkyNTYwYyJ9fSwib3JnX2lkIjoib3JnX2FjbWUiLCJ1c2VyX2F1dGhvcml6YXRpb24iOnsiYWdlbnRzIjpbImNvZGVyIiwiYnJvd3NlciIsIndlYi1zZWFyY2giLCJyZXBhaXItYWdlbnQiXSwiZXhwIjoiMjAyNi0wNi0xNFQwOTowMDowMFoiLCJpYXQiOiIyMDI2LTA1LTE0VDA5OjAwOjAwWiIsIm5vbmNlIjoiOWY0ZTFjOGIyYTNkNGU1ZjZhN2I4YzlkMGUxZjJhM2IiLCJvcmdfc2lnIjp7ImFsZyI6ImVjZHNhLXNlY3AyNTZrMS1zaGEyNTYiLCJzaWciOiJjODFkYTk2Zjk4ODFkYjFmYTg2ZjA0Y2E0MGM0MDNkMzNkMGUwMDIwYzVjNThlMjJjYjdmMjdhNjk1YWVjNmU2NTBkOTM4M2FmMTEwMWFjMTMzMzhmODY3OTA1NjkzZjQxMjk4NjFlODhkNjlkMDdjYmNiOTg2NjYyNjM4ZDhjOSJ9LCJ1c2VyX2lkIjoidV9hbGljZSIsInVzZXJfcHVia2V5Ijp7ImFsZyI6ImVkMjU1MTkiLCJrZXkiOiJkMDRhYjIzMjc0MmJiNGFiM2ExMzY4YmQ0NjE1ZTRlNmQwMjI0YWI3MWEwMTZiYWY4NTIwYTMzMmM5Nzc4NzM3In19LCJ2IjoxfQ", "attenuation_hmac_inputs": [], "claims": { "org_id": "org_acme", "user_id": "u_alice", - "realm": "w1", "agent_name": "coder", "run_id": "r_01h8alpharootinvocation00", "effective_caveats": { @@ -67,10 +59,12 @@ ], "max_cost_usd": 5, "max_steps": 100, + "budget": null, "exp": "2026-05-14T10:10:00Z" }, "ua_nonce": "9f4e1c8b2a3d4e5f6a7b8c9d0e1f2a3b", "ua_budget": null, + "permitted_realms": null, "nonces": [ "9f4e1c8b2a3d4e5f6a7b8c9d0e1f2a3b", "7c2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c" diff --git a/gateway/auth/fixtures/02-one-attenuation.json b/gateway/auth/fixtures/02-one-attenuation.json index 562395c40..779590691 100644 --- a/gateway/auth/fixtures/02-one-attenuation.json +++ b/gateway/auth/fixtures/02-one-attenuation.json @@ -1,5 +1,5 @@ { - "description": "single-key org with one sub-agent attenuation narrowing budget and adding a web-search agent", + "description": "single-key org with one sub-agent attenuation narrowing budget and adding a web-search agent (lineage extension)", "inputs": { "org_id": "org_acme", "org_priv_hex": "0000000000000000000000000000000000000000000000000000000000000001", @@ -17,24 +17,17 @@ "alg": "ed25519", "key": "d04ab232742bb4ab3a1368bd4615e4e6d0224ab71a016baf8520a332c9778737" }, - "permissions": { - "realms": [ - "w1", - "w2" - ], - "agents": [ - "coder", - "browser", - "web-search", - "repair-agent" - ] - }, + "agents": [ + "coder", + "browser", + "web-search", + "repair-agent" + ], "iat": "2026-05-14T09:00:00Z", "exp": "2026-06-14T09:00:00Z", "nonce": "9f4e1c8b2a3d4e5f6a7b8c9d0e1f2a3b" }, "inv_unsigned": { - "realm": "w1", "agents": [ "coder" ], @@ -60,24 +53,23 @@ ] }, "expected": { - "ua_signing_bytes_hex": "7b22657870223a22323032362d30362d31345430393a30303a30305a222c22696174223a22323032362d30352d31345430393a30303a30305a222c226e6f6e6365223a223966346531633862326133643465356636613762386339643065316632613362222c227065726d697373696f6e73223a7b226167656e7473223a5b22636f646572222c2262726f77736572222c227765622d736561726368222c227265706169722d6167656e74225d2c227265616c6d73223a5b227731222c227732225d7d2c22757365725f6964223a22755f616c696365222c22757365725f7075626b6579223a7b22616c67223a2265643235353139222c226b6579223a2264303461623233323734326262346162336131333638626434363135653465366430323234616237316130313662616638353230613333326339373738373337227d7d", - "inv_signing_bytes_hex": "7b226167656e7473223a5b22636f646572225d2c22657870223a22323032362d30352d31345431303a31303a30305a222c22696174223a22323032362d30352d31345431303a30303a30305a222c226d61785f636f73745f757364223a352c226d61785f7374657073223a3130302c226e6f6e6365223a223763326133623463356436653766386139623063316432653366346135623663222c227265616c6d223a227731222c2272756e5f6964223a22725f30316838616c706861726f6f74696e766f636174696f6e3030227d", - "ua_canonical_json": "{\"exp\":\"2026-06-14T09:00:00Z\",\"iat\":\"2026-05-14T09:00:00Z\",\"nonce\":\"9f4e1c8b2a3d4e5f6a7b8c9d0e1f2a3b\",\"permissions\":{\"agents\":[\"coder\",\"browser\",\"web-search\",\"repair-agent\"],\"realms\":[\"w1\",\"w2\"]},\"user_id\":\"u_alice\",\"user_pubkey\":{\"alg\":\"ed25519\",\"key\":\"d04ab232742bb4ab3a1368bd4615e4e6d0224ab71a016baf8520a332c9778737\"}}", - "inv_canonical_json": "{\"agents\":[\"coder\"],\"exp\":\"2026-05-14T10:10:00Z\",\"iat\":\"2026-05-14T10:00:00Z\",\"max_cost_usd\":5,\"max_steps\":100,\"nonce\":\"7c2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c\",\"realm\":\"w1\",\"run_id\":\"r_01h8alpharootinvocation00\"}", - "macaroon_canonical_json": "{\"attenuations\":[{\"caveats\":{\"agents\":[\"coder\",\"web-search\"],\"exp\":\"2026-05-14T10:02:00Z\",\"max_cost_usd\":2,\"max_steps\":40,\"nonce\":\"1e8d2c3b4a5f6e7d8c9b0a1f2e3d4c5b\",\"run_id\":\"r_01h8alphachildlevel1run00\"},\"hmac\":\"6e81acf116db5dc07b33ff5aee4b6c78d40a637ad132a4fdec5f35642a864c61\"}],\"invocation\":{\"agents\":[\"coder\"],\"exp\":\"2026-05-14T10:10:00Z\",\"iat\":\"2026-05-14T10:00:00Z\",\"max_cost_usd\":5,\"max_steps\":100,\"nonce\":\"7c2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c\",\"realm\":\"w1\",\"run_id\":\"r_01h8alpharootinvocation00\",\"user_sig\":{\"alg\":\"ed25519\",\"sig\":\"9835547aa1f05a02b129e3a8f39a1389d2bee62b15c8996f6d9776bab6f68853830042e118f7cf1bcfc087e57d8c48115325d1370e3f494bd02b558c76d97202\"}},\"org_id\":\"org_acme\",\"user_authorization\":{\"exp\":\"2026-06-14T09:00:00Z\",\"iat\":\"2026-05-14T09:00:00Z\",\"nonce\":\"9f4e1c8b2a3d4e5f6a7b8c9d0e1f2a3b\",\"org_sig\":{\"alg\":\"ecdsa-secp256k1-sha256\",\"sig\":\"8f1dc45d5fb83ea7f4f06e11ba2a7540f2ecbca0aa8c0c0b357b24c35ff4c70017b23592b91b174f2c0540312d94a4b90e8a6477518a88436039405bcdcd4e5b\"},\"permissions\":{\"agents\":[\"coder\",\"browser\",\"web-search\",\"repair-agent\"],\"realms\":[\"w1\",\"w2\"]},\"user_id\":\"u_alice\",\"user_pubkey\":{\"alg\":\"ed25519\",\"key\":\"d04ab232742bb4ab3a1368bd4615e4e6d0224ab71a016baf8520a332c9778737\"}},\"v\":1}", - "macaroon_b64url": "eyJhdHRlbnVhdGlvbnMiOlt7ImNhdmVhdHMiOnsiYWdlbnRzIjpbImNvZGVyIiwid2ViLXNlYXJjaCJdLCJleHAiOiIyMDI2LTA1LTE0VDEwOjAyOjAwWiIsIm1heF9jb3N0X3VzZCI6MiwibWF4X3N0ZXBzIjo0MCwibm9uY2UiOiIxZThkMmMzYjRhNWY2ZTdkOGM5YjBhMWYyZTNkNGM1YiIsInJ1bl9pZCI6InJfMDFoOGFscGhhY2hpbGRsZXZlbDFydW4wMCJ9LCJobWFjIjoiNmU4MWFjZjExNmRiNWRjMDdiMzNmZjVhZWU0YjZjNzhkNDBhNjM3YWQxMzJhNGZkZWM1ZjM1NjQyYTg2NGM2MSJ9XSwiaW52b2NhdGlvbiI6eyJhZ2VudHMiOlsiY29kZXIiXSwiZXhwIjoiMjAyNi0wNS0xNFQxMDoxMDowMFoiLCJpYXQiOiIyMDI2LTA1LTE0VDEwOjAwOjAwWiIsIm1heF9jb3N0X3VzZCI6NSwibWF4X3N0ZXBzIjoxMDAsIm5vbmNlIjoiN2MyYTNiNGM1ZDZlN2Y4YTliMGMxZDJlM2Y0YTViNmMiLCJyZWFsbSI6IncxIiwicnVuX2lkIjoicl8wMWg4YWxwaGFyb290aW52b2NhdGlvbjAwIiwidXNlcl9zaWciOnsiYWxnIjoiZWQyNTUxOSIsInNpZyI6Ijk4MzU1NDdhYTFmMDVhMDJiMTI5ZTNhOGYzOWExMzg5ZDJiZWU2MmIxNWM4OTk2ZjZkOTc3NmJhYjZmNjg4NTM4MzAwNDJlMTE4ZjdjZjFiY2ZjMDg3ZTU3ZDhjNDgxMTUzMjVkMTM3MGUzZjQ5NGJkMDJiNTU4Yzc2ZDk3MjAyIn19LCJvcmdfaWQiOiJvcmdfYWNtZSIsInVzZXJfYXV0aG9yaXphdGlvbiI6eyJleHAiOiIyMDI2LTA2LTE0VDA5OjAwOjAwWiIsImlhdCI6IjIwMjYtMDUtMTRUMDk6MDA6MDBaIiwibm9uY2UiOiI5ZjRlMWM4YjJhM2Q0ZTVmNmE3YjhjOWQwZTFmMmEzYiIsIm9yZ19zaWciOnsiYWxnIjoiZWNkc2Etc2VjcDI1NmsxLXNoYTI1NiIsInNpZyI6IjhmMWRjNDVkNWZiODNlYTdmNGYwNmUxMWJhMmE3NTQwZjJlY2JjYTBhYThjMGMwYjM1N2IyNGMzNWZmNGM3MDAxN2IyMzU5MmI5MWIxNzRmMmMwNTQwMzEyZDk0YTRiOTBlOGE2NDc3NTE4YTg4NDM2MDM5NDA1YmNkY2Q0ZTViIn0sInBlcm1pc3Npb25zIjp7ImFnZW50cyI6WyJjb2RlciIsImJyb3dzZXIiLCJ3ZWItc2VhcmNoIiwicmVwYWlyLWFnZW50Il0sInJlYWxtcyI6WyJ3MSIsIncyIl19LCJ1c2VyX2lkIjoidV9hbGljZSIsInVzZXJfcHVia2V5Ijp7ImFsZyI6ImVkMjU1MTkiLCJrZXkiOiJkMDRhYjIzMjc0MmJiNGFiM2ExMzY4YmQ0NjE1ZTRlNmQwMjI0YWI3MWEwMTZiYWY4NTIwYTMzMmM5Nzc4NzM3In19LCJ2IjoxfQ", + "ua_signing_bytes_hex": "7b226167656e7473223a5b22636f646572222c2262726f77736572222c227765622d736561726368222c227265706169722d6167656e74225d2c22657870223a22323032362d30362d31345430393a30303a30305a222c22696174223a22323032362d30352d31345430393a30303a30305a222c226e6f6e6365223a223966346531633862326133643465356636613762386339643065316632613362222c22757365725f6964223a22755f616c696365222c22757365725f7075626b6579223a7b22616c67223a2265643235353139222c226b6579223a2264303461623233323734326262346162336131333638626434363135653465366430323234616237316130313662616638353230613333326339373738373337227d7d", + "inv_signing_bytes_hex": "7b226167656e7473223a5b22636f646572225d2c22657870223a22323032362d30352d31345431303a31303a30305a222c22696174223a22323032362d30352d31345431303a30303a30305a222c226d61785f636f73745f757364223a352c226d61785f7374657073223a3130302c226e6f6e6365223a223763326133623463356436653766386139623063316432653366346135623663222c2272756e5f6964223a22725f30316838616c706861726f6f74696e766f636174696f6e3030227d", + "ua_canonical_json": "{\"agents\":[\"coder\",\"browser\",\"web-search\",\"repair-agent\"],\"exp\":\"2026-06-14T09:00:00Z\",\"iat\":\"2026-05-14T09:00:00Z\",\"nonce\":\"9f4e1c8b2a3d4e5f6a7b8c9d0e1f2a3b\",\"user_id\":\"u_alice\",\"user_pubkey\":{\"alg\":\"ed25519\",\"key\":\"d04ab232742bb4ab3a1368bd4615e4e6d0224ab71a016baf8520a332c9778737\"}}", + "inv_canonical_json": "{\"agents\":[\"coder\"],\"exp\":\"2026-05-14T10:10:00Z\",\"iat\":\"2026-05-14T10:00:00Z\",\"max_cost_usd\":5,\"max_steps\":100,\"nonce\":\"7c2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c\",\"run_id\":\"r_01h8alpharootinvocation00\"}", + "macaroon_canonical_json": "{\"attenuations\":[{\"caveats\":{\"agents\":[\"coder\",\"web-search\"],\"exp\":\"2026-05-14T10:02:00Z\",\"max_cost_usd\":2,\"max_steps\":40,\"nonce\":\"1e8d2c3b4a5f6e7d8c9b0a1f2e3d4c5b\",\"run_id\":\"r_01h8alphachildlevel1run00\"},\"hmac\":\"1e26a1a911d44618bb6933a99b551d50fd5f6c8f8ed2cf0f0921c39773b526cc\"}],\"invocation\":{\"agents\":[\"coder\"],\"exp\":\"2026-05-14T10:10:00Z\",\"iat\":\"2026-05-14T10:00:00Z\",\"max_cost_usd\":5,\"max_steps\":100,\"nonce\":\"7c2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c\",\"run_id\":\"r_01h8alpharootinvocation00\",\"user_sig\":{\"alg\":\"ed25519\",\"sig\":\"c82c8f3ec3ac0ef87d5e28a47c578d33729ba630091bbdc767bd75f55e7af84bb30f9c4bce363205ef79e513caca60758a8f1e02d8d520225a6e317a5092560c\"}},\"org_id\":\"org_acme\",\"user_authorization\":{\"agents\":[\"coder\",\"browser\",\"web-search\",\"repair-agent\"],\"exp\":\"2026-06-14T09:00:00Z\",\"iat\":\"2026-05-14T09:00:00Z\",\"nonce\":\"9f4e1c8b2a3d4e5f6a7b8c9d0e1f2a3b\",\"org_sig\":{\"alg\":\"ecdsa-secp256k1-sha256\",\"sig\":\"c81da96f9881db1fa86f04ca40c403d33d0e0020c5c58e22cb7f27a695aec6e650d9383af1101ac13338f867905693f4129861e88d69d07cbcb986662638d8c9\"},\"user_id\":\"u_alice\",\"user_pubkey\":{\"alg\":\"ed25519\",\"key\":\"d04ab232742bb4ab3a1368bd4615e4e6d0224ab71a016baf8520a332c9778737\"}},\"v\":1}", + "macaroon_b64url": "eyJhdHRlbnVhdGlvbnMiOlt7ImNhdmVhdHMiOnsiYWdlbnRzIjpbImNvZGVyIiwid2ViLXNlYXJjaCJdLCJleHAiOiIyMDI2LTA1LTE0VDEwOjAyOjAwWiIsIm1heF9jb3N0X3VzZCI6MiwibWF4X3N0ZXBzIjo0MCwibm9uY2UiOiIxZThkMmMzYjRhNWY2ZTdkOGM5YjBhMWYyZTNkNGM1YiIsInJ1bl9pZCI6InJfMDFoOGFscGhhY2hpbGRsZXZlbDFydW4wMCJ9LCJobWFjIjoiMWUyNmExYTkxMWQ0NDYxOGJiNjkzM2E5OWI1NTFkNTBmZDVmNmM4ZjhlZDJjZjBmMDkyMWMzOTc3M2I1MjZjYyJ9XSwiaW52b2NhdGlvbiI6eyJhZ2VudHMiOlsiY29kZXIiXSwiZXhwIjoiMjAyNi0wNS0xNFQxMDoxMDowMFoiLCJpYXQiOiIyMDI2LTA1LTE0VDEwOjAwOjAwWiIsIm1heF9jb3N0X3VzZCI6NSwibWF4X3N0ZXBzIjoxMDAsIm5vbmNlIjoiN2MyYTNiNGM1ZDZlN2Y4YTliMGMxZDJlM2Y0YTViNmMiLCJydW5faWQiOiJyXzAxaDhhbHBoYXJvb3RpbnZvY2F0aW9uMDAiLCJ1c2VyX3NpZyI6eyJhbGciOiJlZDI1NTE5Iiwic2lnIjoiYzgyYzhmM2VjM2FjMGVmODdkNWUyOGE0N2M1NzhkMzM3MjliYTYzMDA5MWJiZGM3NjdiZDc1ZjU1ZTdhZjg0YmIzMGY5YzRiY2UzNjMyMDVlZjc5ZTUxM2NhY2E2MDc1OGE4ZjFlMDJkOGQ1MjAyMjVhNmUzMTdhNTA5MjU2MGMifX0sIm9yZ19pZCI6Im9yZ19hY21lIiwidXNlcl9hdXRob3JpemF0aW9uIjp7ImFnZW50cyI6WyJjb2RlciIsImJyb3dzZXIiLCJ3ZWItc2VhcmNoIiwicmVwYWlyLWFnZW50Il0sImV4cCI6IjIwMjYtMDYtMTRUMDk6MDA6MDBaIiwiaWF0IjoiMjAyNi0wNS0xNFQwOTowMDowMFoiLCJub25jZSI6IjlmNGUxYzhiMmEzZDRlNWY2YTdiOGM5ZDBlMWYyYTNiIiwib3JnX3NpZyI6eyJhbGciOiJlY2RzYS1zZWNwMjU2azEtc2hhMjU2Iiwic2lnIjoiYzgxZGE5NmY5ODgxZGIxZmE4NmYwNGNhNDBjNDAzZDMzZDBlMDAyMGM1YzU4ZTIyY2I3ZjI3YTY5NWFlYzZlNjUwZDkzODNhZjExMDFhYzEzMzM4Zjg2NzkwNTY5M2Y0MTI5ODYxZTg4ZDY5ZDA3Y2JjYjk4NjY2MjYzOGQ4YzkifSwidXNlcl9pZCI6InVfYWxpY2UiLCJ1c2VyX3B1YmtleSI6eyJhbGciOiJlZDI1NTE5Iiwia2V5IjoiZDA0YWIyMzI3NDJiYjRhYjNhMTM2OGJkNDYxNWU0ZTZkMDIyNGFiNzFhMDE2YmFmODUyMGEzMzJjOTc3ODczNyJ9fSwidiI6MX0", "attenuation_hmac_inputs": [ { - "prev_sig_hex": "9835547aa1f05a02b129e3a8f39a1389d2bee62b15c8996f6d9776bab6f68853830042e118f7cf1bcfc087e57d8c48115325d1370e3f494bd02b558c76d97202", + "prev_sig_hex": "c82c8f3ec3ac0ef87d5e28a47c578d33729ba630091bbdc767bd75f55e7af84bb30f9c4bce363205ef79e513caca60758a8f1e02d8d520225a6e317a5092560c", "caveats_canonical_json": "{\"agents\":[\"coder\",\"web-search\"],\"exp\":\"2026-05-14T10:02:00Z\",\"max_cost_usd\":2,\"max_steps\":40,\"nonce\":\"1e8d2c3b4a5f6e7d8c9b0a1f2e3d4c5b\",\"run_id\":\"r_01h8alphachildlevel1run00\"}", "hmac_input_hex": "7b226167656e7473223a5b22636f646572222c227765622d736561726368225d2c22657870223a22323032362d30352d31345431303a30323a30305a222c226d61785f636f73745f757364223a322c226d61785f7374657073223a34302c226e6f6e6365223a223165386432633362346135663665376438633962306131663265336434633562222c2272756e5f6964223a22725f30316838616c7068616368696c646c6576656c3172756e3030227d", - "hmac_output_hex": "6e81acf116db5dc07b33ff5aee4b6c78d40a637ad132a4fdec5f35642a864c61" + "hmac_output_hex": "1e26a1a911d44618bb6933a99b551d50fd5f6c8f8ed2cf0f0921c39773b526cc" } ], "claims": { "org_id": "org_acme", "user_id": "u_alice", - "realm": "w1", "agent_name": "web-search", "run_id": "r_01h8alphachildlevel1run00", "effective_caveats": { @@ -87,10 +79,12 @@ ], "max_cost_usd": 2, "max_steps": 40, + "budget": null, "exp": "2026-05-14T10:02:00Z" }, "ua_nonce": "9f4e1c8b2a3d4e5f6a7b8c9d0e1f2a3b", "ua_budget": null, + "permitted_realms": null, "nonces": [ "9f4e1c8b2a3d4e5f6a7b8c9d0e1f2a3b", "7c2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c", diff --git a/gateway/auth/fixtures/03-two-attenuations.json b/gateway/auth/fixtures/03-two-attenuations.json index f7210d633..b9becd029 100644 --- a/gateway/auth/fixtures/03-two-attenuations.json +++ b/gateway/auth/fixtures/03-two-attenuations.json @@ -17,24 +17,17 @@ "alg": "ed25519", "key": "d04ab232742bb4ab3a1368bd4615e4e6d0224ab71a016baf8520a332c9778737" }, - "permissions": { - "realms": [ - "w1", - "w2" - ], - "agents": [ - "coder", - "browser", - "web-search", - "repair-agent" - ] - }, + "agents": [ + "coder", + "browser", + "web-search", + "repair-agent" + ], "iat": "2026-05-14T09:00:00Z", "exp": "2026-06-14T09:00:00Z", "nonce": "9f4e1c8b2a3d4e5f6a7b8c9d0e1f2a3b" }, "inv_unsigned": { - "realm": "w1", "agents": [ "coder" ], @@ -72,30 +65,29 @@ ] }, "expected": { - "ua_signing_bytes_hex": "7b22657870223a22323032362d30362d31345430393a30303a30305a222c22696174223a22323032362d30352d31345430393a30303a30305a222c226e6f6e6365223a223966346531633862326133643465356636613762386339643065316632613362222c227065726d697373696f6e73223a7b226167656e7473223a5b22636f646572222c2262726f77736572222c227765622d736561726368222c227265706169722d6167656e74225d2c227265616c6d73223a5b227731222c227732225d7d2c22757365725f6964223a22755f616c696365222c22757365725f7075626b6579223a7b22616c67223a2265643235353139222c226b6579223a2264303461623233323734326262346162336131333638626434363135653465366430323234616237316130313662616638353230613333326339373738373337227d7d", - "inv_signing_bytes_hex": "7b226167656e7473223a5b22636f646572225d2c22657870223a22323032362d30352d31345431303a31303a30305a222c22696174223a22323032362d30352d31345431303a30303a30305a222c226d61785f636f73745f757364223a352c226d61785f7374657073223a3130302c226e6f6e6365223a223763326133623463356436653766386139623063316432653366346135623663222c227265616c6d223a227731222c2272756e5f6964223a22725f30316838616c706861726f6f74696e766f636174696f6e3030227d", - "ua_canonical_json": "{\"exp\":\"2026-06-14T09:00:00Z\",\"iat\":\"2026-05-14T09:00:00Z\",\"nonce\":\"9f4e1c8b2a3d4e5f6a7b8c9d0e1f2a3b\",\"permissions\":{\"agents\":[\"coder\",\"browser\",\"web-search\",\"repair-agent\"],\"realms\":[\"w1\",\"w2\"]},\"user_id\":\"u_alice\",\"user_pubkey\":{\"alg\":\"ed25519\",\"key\":\"d04ab232742bb4ab3a1368bd4615e4e6d0224ab71a016baf8520a332c9778737\"}}", - "inv_canonical_json": "{\"agents\":[\"coder\"],\"exp\":\"2026-05-14T10:10:00Z\",\"iat\":\"2026-05-14T10:00:00Z\",\"max_cost_usd\":5,\"max_steps\":100,\"nonce\":\"7c2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c\",\"realm\":\"w1\",\"run_id\":\"r_01h8alpharootinvocation00\"}", - "macaroon_canonical_json": "{\"attenuations\":[{\"caveats\":{\"agents\":[\"coder\",\"web-search\"],\"exp\":\"2026-05-14T10:02:00Z\",\"max_cost_usd\":2,\"max_steps\":40,\"nonce\":\"1e8d2c3b4a5f6e7d8c9b0a1f2e3d4c5b\",\"run_id\":\"r_01h8alphachildlevel1run00\"},\"hmac\":\"6e81acf116db5dc07b33ff5aee4b6c78d40a637ad132a4fdec5f35642a864c61\"},{\"caveats\":{\"agents\":[\"coder\",\"web-search\",\"browser\"],\"exp\":\"2026-05-14T10:01:30Z\",\"max_cost_usd\":0.5,\"max_steps\":10,\"nonce\":\"2f9e3d4c5b6a7f8e9d0c1b2a3f4e5d6c\",\"run_id\":\"r_01h8alphagrandchild2run00\"},\"hmac\":\"519b64298fbc1b0faf362a73453e88f150f9c49f04acaffd5a6a5174d1e1f850\"}],\"invocation\":{\"agents\":[\"coder\"],\"exp\":\"2026-05-14T10:10:00Z\",\"iat\":\"2026-05-14T10:00:00Z\",\"max_cost_usd\":5,\"max_steps\":100,\"nonce\":\"7c2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c\",\"realm\":\"w1\",\"run_id\":\"r_01h8alpharootinvocation00\",\"user_sig\":{\"alg\":\"ed25519\",\"sig\":\"9835547aa1f05a02b129e3a8f39a1389d2bee62b15c8996f6d9776bab6f68853830042e118f7cf1bcfc087e57d8c48115325d1370e3f494bd02b558c76d97202\"}},\"org_id\":\"org_acme\",\"user_authorization\":{\"exp\":\"2026-06-14T09:00:00Z\",\"iat\":\"2026-05-14T09:00:00Z\",\"nonce\":\"9f4e1c8b2a3d4e5f6a7b8c9d0e1f2a3b\",\"org_sig\":{\"alg\":\"ecdsa-secp256k1-sha256\",\"sig\":\"8f1dc45d5fb83ea7f4f06e11ba2a7540f2ecbca0aa8c0c0b357b24c35ff4c70017b23592b91b174f2c0540312d94a4b90e8a6477518a88436039405bcdcd4e5b\"},\"permissions\":{\"agents\":[\"coder\",\"browser\",\"web-search\",\"repair-agent\"],\"realms\":[\"w1\",\"w2\"]},\"user_id\":\"u_alice\",\"user_pubkey\":{\"alg\":\"ed25519\",\"key\":\"d04ab232742bb4ab3a1368bd4615e4e6d0224ab71a016baf8520a332c9778737\"}},\"v\":1}", - "macaroon_b64url": "eyJhdHRlbnVhdGlvbnMiOlt7ImNhdmVhdHMiOnsiYWdlbnRzIjpbImNvZGVyIiwid2ViLXNlYXJjaCJdLCJleHAiOiIyMDI2LTA1LTE0VDEwOjAyOjAwWiIsIm1heF9jb3N0X3VzZCI6MiwibWF4X3N0ZXBzIjo0MCwibm9uY2UiOiIxZThkMmMzYjRhNWY2ZTdkOGM5YjBhMWYyZTNkNGM1YiIsInJ1bl9pZCI6InJfMDFoOGFscGhhY2hpbGRsZXZlbDFydW4wMCJ9LCJobWFjIjoiNmU4MWFjZjExNmRiNWRjMDdiMzNmZjVhZWU0YjZjNzhkNDBhNjM3YWQxMzJhNGZkZWM1ZjM1NjQyYTg2NGM2MSJ9LHsiY2F2ZWF0cyI6eyJhZ2VudHMiOlsiY29kZXIiLCJ3ZWItc2VhcmNoIiwiYnJvd3NlciJdLCJleHAiOiIyMDI2LTA1LTE0VDEwOjAxOjMwWiIsIm1heF9jb3N0X3VzZCI6MC41LCJtYXhfc3RlcHMiOjEwLCJub25jZSI6IjJmOWUzZDRjNWI2YTdmOGU5ZDBjMWIyYTNmNGU1ZDZjIiwicnVuX2lkIjoicl8wMWg4YWxwaGFncmFuZGNoaWxkMnJ1bjAwIn0sImhtYWMiOiI1MTliNjQyOThmYmMxYjBmYWYzNjJhNzM0NTNlODhmMTUwZjljNDlmMDRhY2FmZmQ1YTZhNTE3NGQxZTFmODUwIn1dLCJpbnZvY2F0aW9uIjp7ImFnZW50cyI6WyJjb2RlciJdLCJleHAiOiIyMDI2LTA1LTE0VDEwOjEwOjAwWiIsImlhdCI6IjIwMjYtMDUtMTRUMTA6MDA6MDBaIiwibWF4X2Nvc3RfdXNkIjo1LCJtYXhfc3RlcHMiOjEwMCwibm9uY2UiOiI3YzJhM2I0YzVkNmU3ZjhhOWIwYzFkMmUzZjRhNWI2YyIsInJlYWxtIjoidzEiLCJydW5faWQiOiJyXzAxaDhhbHBoYXJvb3RpbnZvY2F0aW9uMDAiLCJ1c2VyX3NpZyI6eyJhbGciOiJlZDI1NTE5Iiwic2lnIjoiOTgzNTU0N2FhMWYwNWEwMmIxMjllM2E4ZjM5YTEzODlkMmJlZTYyYjE1Yzg5OTZmNmQ5Nzc2YmFiNmY2ODg1MzgzMDA0MmUxMThmN2NmMWJjZmMwODdlNTdkOGM0ODExNTMyNWQxMzcwZTNmNDk0YmQwMmI1NThjNzZkOTcyMDIifX0sIm9yZ19pZCI6Im9yZ19hY21lIiwidXNlcl9hdXRob3JpemF0aW9uIjp7ImV4cCI6IjIwMjYtMDYtMTRUMDk6MDA6MDBaIiwiaWF0IjoiMjAyNi0wNS0xNFQwOTowMDowMFoiLCJub25jZSI6IjlmNGUxYzhiMmEzZDRlNWY2YTdiOGM5ZDBlMWYyYTNiIiwib3JnX3NpZyI6eyJhbGciOiJlY2RzYS1zZWNwMjU2azEtc2hhMjU2Iiwic2lnIjoiOGYxZGM0NWQ1ZmI4M2VhN2Y0ZjA2ZTExYmEyYTc1NDBmMmVjYmNhMGFhOGMwYzBiMzU3YjI0YzM1ZmY0YzcwMDE3YjIzNTkyYjkxYjE3NGYyYzA1NDAzMTJkOTRhNGI5MGU4YTY0Nzc1MThhODg0MzYwMzk0MDViY2RjZDRlNWIifSwicGVybWlzc2lvbnMiOnsiYWdlbnRzIjpbImNvZGVyIiwiYnJvd3NlciIsIndlYi1zZWFyY2giLCJyZXBhaXItYWdlbnQiXSwicmVhbG1zIjpbIncxIiwidzIiXX0sInVzZXJfaWQiOiJ1X2FsaWNlIiwidXNlcl9wdWJrZXkiOnsiYWxnIjoiZWQyNTUxOSIsImtleSI6ImQwNGFiMjMyNzQyYmI0YWIzYTEzNjhiZDQ2MTVlNGU2ZDAyMjRhYjcxYTAxNmJhZjg1MjBhMzMyYzk3Nzg3MzcifX0sInYiOjF9", + "ua_signing_bytes_hex": "7b226167656e7473223a5b22636f646572222c2262726f77736572222c227765622d736561726368222c227265706169722d6167656e74225d2c22657870223a22323032362d30362d31345430393a30303a30305a222c22696174223a22323032362d30352d31345430393a30303a30305a222c226e6f6e6365223a223966346531633862326133643465356636613762386339643065316632613362222c22757365725f6964223a22755f616c696365222c22757365725f7075626b6579223a7b22616c67223a2265643235353139222c226b6579223a2264303461623233323734326262346162336131333638626434363135653465366430323234616237316130313662616638353230613333326339373738373337227d7d", + "inv_signing_bytes_hex": "7b226167656e7473223a5b22636f646572225d2c22657870223a22323032362d30352d31345431303a31303a30305a222c22696174223a22323032362d30352d31345431303a30303a30305a222c226d61785f636f73745f757364223a352c226d61785f7374657073223a3130302c226e6f6e6365223a223763326133623463356436653766386139623063316432653366346135623663222c2272756e5f6964223a22725f30316838616c706861726f6f74696e766f636174696f6e3030227d", + "ua_canonical_json": "{\"agents\":[\"coder\",\"browser\",\"web-search\",\"repair-agent\"],\"exp\":\"2026-06-14T09:00:00Z\",\"iat\":\"2026-05-14T09:00:00Z\",\"nonce\":\"9f4e1c8b2a3d4e5f6a7b8c9d0e1f2a3b\",\"user_id\":\"u_alice\",\"user_pubkey\":{\"alg\":\"ed25519\",\"key\":\"d04ab232742bb4ab3a1368bd4615e4e6d0224ab71a016baf8520a332c9778737\"}}", + "inv_canonical_json": "{\"agents\":[\"coder\"],\"exp\":\"2026-05-14T10:10:00Z\",\"iat\":\"2026-05-14T10:00:00Z\",\"max_cost_usd\":5,\"max_steps\":100,\"nonce\":\"7c2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c\",\"run_id\":\"r_01h8alpharootinvocation00\"}", + "macaroon_canonical_json": "{\"attenuations\":[{\"caveats\":{\"agents\":[\"coder\",\"web-search\"],\"exp\":\"2026-05-14T10:02:00Z\",\"max_cost_usd\":2,\"max_steps\":40,\"nonce\":\"1e8d2c3b4a5f6e7d8c9b0a1f2e3d4c5b\",\"run_id\":\"r_01h8alphachildlevel1run00\"},\"hmac\":\"1e26a1a911d44618bb6933a99b551d50fd5f6c8f8ed2cf0f0921c39773b526cc\"},{\"caveats\":{\"agents\":[\"coder\",\"web-search\",\"browser\"],\"exp\":\"2026-05-14T10:01:30Z\",\"max_cost_usd\":0.5,\"max_steps\":10,\"nonce\":\"2f9e3d4c5b6a7f8e9d0c1b2a3f4e5d6c\",\"run_id\":\"r_01h8alphagrandchild2run00\"},\"hmac\":\"8ccf8310144c507b01165190a94f24128edb0859dd34c929ed15ae35b46a4175\"}],\"invocation\":{\"agents\":[\"coder\"],\"exp\":\"2026-05-14T10:10:00Z\",\"iat\":\"2026-05-14T10:00:00Z\",\"max_cost_usd\":5,\"max_steps\":100,\"nonce\":\"7c2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c\",\"run_id\":\"r_01h8alpharootinvocation00\",\"user_sig\":{\"alg\":\"ed25519\",\"sig\":\"c82c8f3ec3ac0ef87d5e28a47c578d33729ba630091bbdc767bd75f55e7af84bb30f9c4bce363205ef79e513caca60758a8f1e02d8d520225a6e317a5092560c\"}},\"org_id\":\"org_acme\",\"user_authorization\":{\"agents\":[\"coder\",\"browser\",\"web-search\",\"repair-agent\"],\"exp\":\"2026-06-14T09:00:00Z\",\"iat\":\"2026-05-14T09:00:00Z\",\"nonce\":\"9f4e1c8b2a3d4e5f6a7b8c9d0e1f2a3b\",\"org_sig\":{\"alg\":\"ecdsa-secp256k1-sha256\",\"sig\":\"c81da96f9881db1fa86f04ca40c403d33d0e0020c5c58e22cb7f27a695aec6e650d9383af1101ac13338f867905693f4129861e88d69d07cbcb986662638d8c9\"},\"user_id\":\"u_alice\",\"user_pubkey\":{\"alg\":\"ed25519\",\"key\":\"d04ab232742bb4ab3a1368bd4615e4e6d0224ab71a016baf8520a332c9778737\"}},\"v\":1}", + "macaroon_b64url": "eyJhdHRlbnVhdGlvbnMiOlt7ImNhdmVhdHMiOnsiYWdlbnRzIjpbImNvZGVyIiwid2ViLXNlYXJjaCJdLCJleHAiOiIyMDI2LTA1LTE0VDEwOjAyOjAwWiIsIm1heF9jb3N0X3VzZCI6MiwibWF4X3N0ZXBzIjo0MCwibm9uY2UiOiIxZThkMmMzYjRhNWY2ZTdkOGM5YjBhMWYyZTNkNGM1YiIsInJ1bl9pZCI6InJfMDFoOGFscGhhY2hpbGRsZXZlbDFydW4wMCJ9LCJobWFjIjoiMWUyNmExYTkxMWQ0NDYxOGJiNjkzM2E5OWI1NTFkNTBmZDVmNmM4ZjhlZDJjZjBmMDkyMWMzOTc3M2I1MjZjYyJ9LHsiY2F2ZWF0cyI6eyJhZ2VudHMiOlsiY29kZXIiLCJ3ZWItc2VhcmNoIiwiYnJvd3NlciJdLCJleHAiOiIyMDI2LTA1LTE0VDEwOjAxOjMwWiIsIm1heF9jb3N0X3VzZCI6MC41LCJtYXhfc3RlcHMiOjEwLCJub25jZSI6IjJmOWUzZDRjNWI2YTdmOGU5ZDBjMWIyYTNmNGU1ZDZjIiwicnVuX2lkIjoicl8wMWg4YWxwaGFncmFuZGNoaWxkMnJ1bjAwIn0sImhtYWMiOiI4Y2NmODMxMDE0NGM1MDdiMDExNjUxOTBhOTRmMjQxMjhlZGIwODU5ZGQzNGM5MjllZDE1YWUzNWI0NmE0MTc1In1dLCJpbnZvY2F0aW9uIjp7ImFnZW50cyI6WyJjb2RlciJdLCJleHAiOiIyMDI2LTA1LTE0VDEwOjEwOjAwWiIsImlhdCI6IjIwMjYtMDUtMTRUMTA6MDA6MDBaIiwibWF4X2Nvc3RfdXNkIjo1LCJtYXhfc3RlcHMiOjEwMCwibm9uY2UiOiI3YzJhM2I0YzVkNmU3ZjhhOWIwYzFkMmUzZjRhNWI2YyIsInJ1bl9pZCI6InJfMDFoOGFscGhhcm9vdGludm9jYXRpb24wMCIsInVzZXJfc2lnIjp7ImFsZyI6ImVkMjU1MTkiLCJzaWciOiJjODJjOGYzZWMzYWMwZWY4N2Q1ZTI4YTQ3YzU3OGQzMzcyOWJhNjMwMDkxYmJkYzc2N2JkNzVmNTVlN2FmODRiYjMwZjljNGJjZTM2MzIwNWVmNzllNTEzY2FjYTYwNzU4YThmMWUwMmQ4ZDUyMDIyNWE2ZTMxN2E1MDkyNTYwYyJ9fSwib3JnX2lkIjoib3JnX2FjbWUiLCJ1c2VyX2F1dGhvcml6YXRpb24iOnsiYWdlbnRzIjpbImNvZGVyIiwiYnJvd3NlciIsIndlYi1zZWFyY2giLCJyZXBhaXItYWdlbnQiXSwiZXhwIjoiMjAyNi0wNi0xNFQwOTowMDowMFoiLCJpYXQiOiIyMDI2LTA1LTE0VDA5OjAwOjAwWiIsIm5vbmNlIjoiOWY0ZTFjOGIyYTNkNGU1ZjZhN2I4YzlkMGUxZjJhM2IiLCJvcmdfc2lnIjp7ImFsZyI6ImVjZHNhLXNlY3AyNTZrMS1zaGEyNTYiLCJzaWciOiJjODFkYTk2Zjk4ODFkYjFmYTg2ZjA0Y2E0MGM0MDNkMzNkMGUwMDIwYzVjNThlMjJjYjdmMjdhNjk1YWVjNmU2NTBkOTM4M2FmMTEwMWFjMTMzMzhmODY3OTA1NjkzZjQxMjk4NjFlODhkNjlkMDdjYmNiOTg2NjYyNjM4ZDhjOSJ9LCJ1c2VyX2lkIjoidV9hbGljZSIsInVzZXJfcHVia2V5Ijp7ImFsZyI6ImVkMjU1MTkiLCJrZXkiOiJkMDRhYjIzMjc0MmJiNGFiM2ExMzY4YmQ0NjE1ZTRlNmQwMjI0YWI3MWEwMTZiYWY4NTIwYTMzMmM5Nzc4NzM3In19LCJ2IjoxfQ", "attenuation_hmac_inputs": [ { - "prev_sig_hex": "9835547aa1f05a02b129e3a8f39a1389d2bee62b15c8996f6d9776bab6f68853830042e118f7cf1bcfc087e57d8c48115325d1370e3f494bd02b558c76d97202", + "prev_sig_hex": "c82c8f3ec3ac0ef87d5e28a47c578d33729ba630091bbdc767bd75f55e7af84bb30f9c4bce363205ef79e513caca60758a8f1e02d8d520225a6e317a5092560c", "caveats_canonical_json": "{\"agents\":[\"coder\",\"web-search\"],\"exp\":\"2026-05-14T10:02:00Z\",\"max_cost_usd\":2,\"max_steps\":40,\"nonce\":\"1e8d2c3b4a5f6e7d8c9b0a1f2e3d4c5b\",\"run_id\":\"r_01h8alphachildlevel1run00\"}", "hmac_input_hex": "7b226167656e7473223a5b22636f646572222c227765622d736561726368225d2c22657870223a22323032362d30352d31345431303a30323a30305a222c226d61785f636f73745f757364223a322c226d61785f7374657073223a34302c226e6f6e6365223a223165386432633362346135663665376438633962306131663265336434633562222c2272756e5f6964223a22725f30316838616c7068616368696c646c6576656c3172756e3030227d", - "hmac_output_hex": "6e81acf116db5dc07b33ff5aee4b6c78d40a637ad132a4fdec5f35642a864c61" + "hmac_output_hex": "1e26a1a911d44618bb6933a99b551d50fd5f6c8f8ed2cf0f0921c39773b526cc" }, { - "prev_sig_hex": "6e81acf116db5dc07b33ff5aee4b6c78d40a637ad132a4fdec5f35642a864c61", + "prev_sig_hex": "1e26a1a911d44618bb6933a99b551d50fd5f6c8f8ed2cf0f0921c39773b526cc", "caveats_canonical_json": "{\"agents\":[\"coder\",\"web-search\",\"browser\"],\"exp\":\"2026-05-14T10:01:30Z\",\"max_cost_usd\":0.5,\"max_steps\":10,\"nonce\":\"2f9e3d4c5b6a7f8e9d0c1b2a3f4e5d6c\",\"run_id\":\"r_01h8alphagrandchild2run00\"}", "hmac_input_hex": "7b226167656e7473223a5b22636f646572222c227765622d736561726368222c2262726f77736572225d2c22657870223a22323032362d30352d31345431303a30313a33305a222c226d61785f636f73745f757364223a302e352c226d61785f7374657073223a31302c226e6f6e6365223a223266396533643463356236613766386539643063316232613366346535643663222c2272756e5f6964223a22725f30316838616c7068616772616e646368696c643272756e3030227d", - "hmac_output_hex": "519b64298fbc1b0faf362a73453e88f150f9c49f04acaffd5a6a5174d1e1f850" + "hmac_output_hex": "8ccf8310144c507b01165190a94f24128edb0859dd34c929ed15ae35b46a4175" } ], "claims": { "org_id": "org_acme", "user_id": "u_alice", - "realm": "w1", "agent_name": "browser", "run_id": "r_01h8alphagrandchild2run00", "effective_caveats": { @@ -106,10 +98,12 @@ ], "max_cost_usd": 0.5, "max_steps": 10, + "budget": null, "exp": "2026-05-14T10:01:30Z" }, "ua_nonce": "9f4e1c8b2a3d4e5f6a7b8c9d0e1f2a3b", "ua_budget": null, + "permitted_realms": null, "nonces": [ "9f4e1c8b2a3d4e5f6a7b8c9d0e1f2a3b", "7c2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c", diff --git a/gateway/auth/fixtures/04-multisig-2of3.json b/gateway/auth/fixtures/04-multisig-2of3.json index 3d3527d77..3f50ace61 100644 --- a/gateway/auth/fixtures/04-multisig-2of3.json +++ b/gateway/auth/fixtures/04-multisig-2of3.json @@ -36,24 +36,17 @@ "alg": "ed25519", "key": "d04ab232742bb4ab3a1368bd4615e4e6d0224ab71a016baf8520a332c9778737" }, - "permissions": { - "realms": [ - "w1", - "w2" - ], - "agents": [ - "coder", - "browser", - "web-search", - "repair-agent" - ] - }, + "agents": [ + "coder", + "browser", + "web-search", + "repair-agent" + ], "iat": "2026-05-14T09:00:00Z", "exp": "2026-06-14T09:00:00Z", "nonce": "9f4e1c8b2a3d4e5f6a7b8c9d0e1f2a3b" }, "inv_unsigned": { - "realm": "w1", "agents": [ "coder" ], @@ -67,17 +60,16 @@ "atts_unsigned": [] }, "expected": { - "ua_signing_bytes_hex": "7b22657870223a22323032362d30362d31345430393a30303a30305a222c22696174223a22323032362d30352d31345430393a30303a30305a222c226e6f6e6365223a223966346531633862326133643465356636613762386339643065316632613362222c227065726d697373696f6e73223a7b226167656e7473223a5b22636f646572222c2262726f77736572222c227765622d736561726368222c227265706169722d6167656e74225d2c227265616c6d73223a5b227731222c227732225d7d2c22757365725f6964223a22755f616c696365222c22757365725f7075626b6579223a7b22616c67223a2265643235353139222c226b6579223a2264303461623233323734326262346162336131333638626434363135653465366430323234616237316130313662616638353230613333326339373738373337227d7d", - "inv_signing_bytes_hex": "7b226167656e7473223a5b22636f646572225d2c22657870223a22323032362d30352d31345431303a31303a30305a222c22696174223a22323032362d30352d31345431303a30303a30305a222c226d61785f636f73745f757364223a352c226d61785f7374657073223a3130302c226e6f6e6365223a223763326133623463356436653766386139623063316432653366346135623663222c227265616c6d223a227731222c2272756e5f6964223a22725f30316838616c706861726f6f74696e766f636174696f6e3030227d", - "ua_canonical_json": "{\"exp\":\"2026-06-14T09:00:00Z\",\"iat\":\"2026-05-14T09:00:00Z\",\"nonce\":\"9f4e1c8b2a3d4e5f6a7b8c9d0e1f2a3b\",\"permissions\":{\"agents\":[\"coder\",\"browser\",\"web-search\",\"repair-agent\"],\"realms\":[\"w1\",\"w2\"]},\"user_id\":\"u_alice\",\"user_pubkey\":{\"alg\":\"ed25519\",\"key\":\"d04ab232742bb4ab3a1368bd4615e4e6d0224ab71a016baf8520a332c9778737\"}}", - "inv_canonical_json": "{\"agents\":[\"coder\"],\"exp\":\"2026-05-14T10:10:00Z\",\"iat\":\"2026-05-14T10:00:00Z\",\"max_cost_usd\":5,\"max_steps\":100,\"nonce\":\"7c2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c\",\"realm\":\"w1\",\"run_id\":\"r_01h8alpharootinvocation00\"}", - "macaroon_canonical_json": "{\"attenuations\":[],\"invocation\":{\"agents\":[\"coder\"],\"exp\":\"2026-05-14T10:10:00Z\",\"iat\":\"2026-05-14T10:00:00Z\",\"max_cost_usd\":5,\"max_steps\":100,\"nonce\":\"7c2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c\",\"realm\":\"w1\",\"run_id\":\"r_01h8alpharootinvocation00\",\"user_sig\":{\"alg\":\"ed25519\",\"sig\":\"9835547aa1f05a02b129e3a8f39a1389d2bee62b15c8996f6d9776bab6f68853830042e118f7cf1bcfc087e57d8c48115325d1370e3f494bd02b558c76d97202\"}},\"org_id\":\"org_multisig\",\"user_authorization\":{\"exp\":\"2026-06-14T09:00:00Z\",\"iat\":\"2026-05-14T09:00:00Z\",\"nonce\":\"9f4e1c8b2a3d4e5f6a7b8c9d0e1f2a3b\",\"org_sig\":{\"alg\":\"multisig-v1\",\"sigs\":[{\"alg\":\"ecdsa-secp256k1-sha256\",\"key_index\":0,\"sig\":\"8f1dc45d5fb83ea7f4f06e11ba2a7540f2ecbca0aa8c0c0b357b24c35ff4c70017b23592b91b174f2c0540312d94a4b90e8a6477518a88436039405bcdcd4e5b\"},{\"alg\":\"ecdsa-secp256k1-sha256\",\"key_index\":2,\"sig\":\"16d6c1a6badd8560aabd14e045f7ab1a9f9c2dcb501c6c47176fea183a6e44136546093c43b523b78dc6148bc3ed630325c698b9a4717e750985d59301cf6b2d\"}]},\"permissions\":{\"agents\":[\"coder\",\"browser\",\"web-search\",\"repair-agent\"],\"realms\":[\"w1\",\"w2\"]},\"user_id\":\"u_alice\",\"user_pubkey\":{\"alg\":\"ed25519\",\"key\":\"d04ab232742bb4ab3a1368bd4615e4e6d0224ab71a016baf8520a332c9778737\"}},\"v\":1}", - "macaroon_b64url": "eyJhdHRlbnVhdGlvbnMiOltdLCJpbnZvY2F0aW9uIjp7ImFnZW50cyI6WyJjb2RlciJdLCJleHAiOiIyMDI2LTA1LTE0VDEwOjEwOjAwWiIsImlhdCI6IjIwMjYtMDUtMTRUMTA6MDA6MDBaIiwibWF4X2Nvc3RfdXNkIjo1LCJtYXhfc3RlcHMiOjEwMCwibm9uY2UiOiI3YzJhM2I0YzVkNmU3ZjhhOWIwYzFkMmUzZjRhNWI2YyIsInJlYWxtIjoidzEiLCJydW5faWQiOiJyXzAxaDhhbHBoYXJvb3RpbnZvY2F0aW9uMDAiLCJ1c2VyX3NpZyI6eyJhbGciOiJlZDI1NTE5Iiwic2lnIjoiOTgzNTU0N2FhMWYwNWEwMmIxMjllM2E4ZjM5YTEzODlkMmJlZTYyYjE1Yzg5OTZmNmQ5Nzc2YmFiNmY2ODg1MzgzMDA0MmUxMThmN2NmMWJjZmMwODdlNTdkOGM0ODExNTMyNWQxMzcwZTNmNDk0YmQwMmI1NThjNzZkOTcyMDIifX0sIm9yZ19pZCI6Im9yZ19tdWx0aXNpZyIsInVzZXJfYXV0aG9yaXphdGlvbiI6eyJleHAiOiIyMDI2LTA2LTE0VDA5OjAwOjAwWiIsImlhdCI6IjIwMjYtMDUtMTRUMDk6MDA6MDBaIiwibm9uY2UiOiI5ZjRlMWM4YjJhM2Q0ZTVmNmE3YjhjOWQwZTFmMmEzYiIsIm9yZ19zaWciOnsiYWxnIjoibXVsdGlzaWctdjEiLCJzaWdzIjpbeyJhbGciOiJlY2RzYS1zZWNwMjU2azEtc2hhMjU2Iiwia2V5X2luZGV4IjowLCJzaWciOiI4ZjFkYzQ1ZDVmYjgzZWE3ZjRmMDZlMTFiYTJhNzU0MGYyZWNiY2EwYWE4YzBjMGIzNTdiMjRjMzVmZjRjNzAwMTdiMjM1OTJiOTFiMTc0ZjJjMDU0MDMxMmQ5NGE0YjkwZThhNjQ3NzUxOGE4ODQzNjAzOTQwNWJjZGNkNGU1YiJ9LHsiYWxnIjoiZWNkc2Etc2VjcDI1NmsxLXNoYTI1NiIsImtleV9pbmRleCI6Miwic2lnIjoiMTZkNmMxYTZiYWRkODU2MGFhYmQxNGUwNDVmN2FiMWE5ZjljMmRjYjUwMWM2YzQ3MTc2ZmVhMTgzYTZlNDQxMzY1NDYwOTNjNDNiNTIzYjc4ZGM2MTQ4YmMzZWQ2MzAzMjVjNjk4YjlhNDcxN2U3NTA5ODVkNTkzMDFjZjZiMmQifV19LCJwZXJtaXNzaW9ucyI6eyJhZ2VudHMiOlsiY29kZXIiLCJicm93c2VyIiwid2ViLXNlYXJjaCIsInJlcGFpci1hZ2VudCJdLCJyZWFsbXMiOlsidzEiLCJ3MiJdfSwidXNlcl9pZCI6InVfYWxpY2UiLCJ1c2VyX3B1YmtleSI6eyJhbGciOiJlZDI1NTE5Iiwia2V5IjoiZDA0YWIyMzI3NDJiYjRhYjNhMTM2OGJkNDYxNWU0ZTZkMDIyNGFiNzFhMDE2YmFmODUyMGEzMzJjOTc3ODczNyJ9fSwidiI6MX0", + "ua_signing_bytes_hex": "7b226167656e7473223a5b22636f646572222c2262726f77736572222c227765622d736561726368222c227265706169722d6167656e74225d2c22657870223a22323032362d30362d31345430393a30303a30305a222c22696174223a22323032362d30352d31345430393a30303a30305a222c226e6f6e6365223a223966346531633862326133643465356636613762386339643065316632613362222c22757365725f6964223a22755f616c696365222c22757365725f7075626b6579223a7b22616c67223a2265643235353139222c226b6579223a2264303461623233323734326262346162336131333638626434363135653465366430323234616237316130313662616638353230613333326339373738373337227d7d", + "inv_signing_bytes_hex": "7b226167656e7473223a5b22636f646572225d2c22657870223a22323032362d30352d31345431303a31303a30305a222c22696174223a22323032362d30352d31345431303a30303a30305a222c226d61785f636f73745f757364223a352c226d61785f7374657073223a3130302c226e6f6e6365223a223763326133623463356436653766386139623063316432653366346135623663222c2272756e5f6964223a22725f30316838616c706861726f6f74696e766f636174696f6e3030227d", + "ua_canonical_json": "{\"agents\":[\"coder\",\"browser\",\"web-search\",\"repair-agent\"],\"exp\":\"2026-06-14T09:00:00Z\",\"iat\":\"2026-05-14T09:00:00Z\",\"nonce\":\"9f4e1c8b2a3d4e5f6a7b8c9d0e1f2a3b\",\"user_id\":\"u_alice\",\"user_pubkey\":{\"alg\":\"ed25519\",\"key\":\"d04ab232742bb4ab3a1368bd4615e4e6d0224ab71a016baf8520a332c9778737\"}}", + "inv_canonical_json": "{\"agents\":[\"coder\"],\"exp\":\"2026-05-14T10:10:00Z\",\"iat\":\"2026-05-14T10:00:00Z\",\"max_cost_usd\":5,\"max_steps\":100,\"nonce\":\"7c2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c\",\"run_id\":\"r_01h8alpharootinvocation00\"}", + "macaroon_canonical_json": "{\"attenuations\":[],\"invocation\":{\"agents\":[\"coder\"],\"exp\":\"2026-05-14T10:10:00Z\",\"iat\":\"2026-05-14T10:00:00Z\",\"max_cost_usd\":5,\"max_steps\":100,\"nonce\":\"7c2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c\",\"run_id\":\"r_01h8alpharootinvocation00\",\"user_sig\":{\"alg\":\"ed25519\",\"sig\":\"c82c8f3ec3ac0ef87d5e28a47c578d33729ba630091bbdc767bd75f55e7af84bb30f9c4bce363205ef79e513caca60758a8f1e02d8d520225a6e317a5092560c\"}},\"org_id\":\"org_multisig\",\"user_authorization\":{\"agents\":[\"coder\",\"browser\",\"web-search\",\"repair-agent\"],\"exp\":\"2026-06-14T09:00:00Z\",\"iat\":\"2026-05-14T09:00:00Z\",\"nonce\":\"9f4e1c8b2a3d4e5f6a7b8c9d0e1f2a3b\",\"org_sig\":{\"alg\":\"multisig-v1\",\"sigs\":[{\"alg\":\"ecdsa-secp256k1-sha256\",\"key_index\":0,\"sig\":\"c81da96f9881db1fa86f04ca40c403d33d0e0020c5c58e22cb7f27a695aec6e650d9383af1101ac13338f867905693f4129861e88d69d07cbcb986662638d8c9\"},{\"alg\":\"ecdsa-secp256k1-sha256\",\"key_index\":2,\"sig\":\"71a4bbfb13bd4cb0649122a38794ca076e9f489e8c6bcd1689f41fb9e518475035f374cbaad193c2d184a9b39ff7f7f03f02026724fb21f70c15aa4c007ca879\"}]},\"user_id\":\"u_alice\",\"user_pubkey\":{\"alg\":\"ed25519\",\"key\":\"d04ab232742bb4ab3a1368bd4615e4e6d0224ab71a016baf8520a332c9778737\"}},\"v\":1}", + "macaroon_b64url": "eyJhdHRlbnVhdGlvbnMiOltdLCJpbnZvY2F0aW9uIjp7ImFnZW50cyI6WyJjb2RlciJdLCJleHAiOiIyMDI2LTA1LTE0VDEwOjEwOjAwWiIsImlhdCI6IjIwMjYtMDUtMTRUMTA6MDA6MDBaIiwibWF4X2Nvc3RfdXNkIjo1LCJtYXhfc3RlcHMiOjEwMCwibm9uY2UiOiI3YzJhM2I0YzVkNmU3ZjhhOWIwYzFkMmUzZjRhNWI2YyIsInJ1bl9pZCI6InJfMDFoOGFscGhhcm9vdGludm9jYXRpb24wMCIsInVzZXJfc2lnIjp7ImFsZyI6ImVkMjU1MTkiLCJzaWciOiJjODJjOGYzZWMzYWMwZWY4N2Q1ZTI4YTQ3YzU3OGQzMzcyOWJhNjMwMDkxYmJkYzc2N2JkNzVmNTVlN2FmODRiYjMwZjljNGJjZTM2MzIwNWVmNzllNTEzY2FjYTYwNzU4YThmMWUwMmQ4ZDUyMDIyNWE2ZTMxN2E1MDkyNTYwYyJ9fSwib3JnX2lkIjoib3JnX211bHRpc2lnIiwidXNlcl9hdXRob3JpemF0aW9uIjp7ImFnZW50cyI6WyJjb2RlciIsImJyb3dzZXIiLCJ3ZWItc2VhcmNoIiwicmVwYWlyLWFnZW50Il0sImV4cCI6IjIwMjYtMDYtMTRUMDk6MDA6MDBaIiwiaWF0IjoiMjAyNi0wNS0xNFQwOTowMDowMFoiLCJub25jZSI6IjlmNGUxYzhiMmEzZDRlNWY2YTdiOGM5ZDBlMWYyYTNiIiwib3JnX3NpZyI6eyJhbGciOiJtdWx0aXNpZy12MSIsInNpZ3MiOlt7ImFsZyI6ImVjZHNhLXNlY3AyNTZrMS1zaGEyNTYiLCJrZXlfaW5kZXgiOjAsInNpZyI6ImM4MWRhOTZmOTg4MWRiMWZhODZmMDRjYTQwYzQwM2QzM2QwZTAwMjBjNWM1OGUyMmNiN2YyN2E2OTVhZWM2ZTY1MGQ5MzgzYWYxMTAxYWMxMzMzOGY4Njc5MDU2OTNmNDEyOTg2MWU4OGQ2OWQwN2NiY2I5ODY2NjI2MzhkOGM5In0seyJhbGciOiJlY2RzYS1zZWNwMjU2azEtc2hhMjU2Iiwia2V5X2luZGV4IjoyLCJzaWciOiI3MWE0YmJmYjEzYmQ0Y2IwNjQ5MTIyYTM4Nzk0Y2EwNzZlOWY0ODllOGM2YmNkMTY4OWY0MWZiOWU1MTg0NzUwMzVmMzc0Y2JhYWQxOTNjMmQxODRhOWIzOWZmN2Y3ZjAzZjAyMDI2NzI0ZmIyMWY3MGMxNWFhNGMwMDdjYTg3OSJ9XX0sInVzZXJfaWQiOiJ1X2FsaWNlIiwidXNlcl9wdWJrZXkiOnsiYWxnIjoiZWQyNTUxOSIsImtleSI6ImQwNGFiMjMyNzQyYmI0YWIzYTEzNjhiZDQ2MTVlNGU2ZDAyMjRhYjcxYTAxNmJhZjg1MjBhMzMyYzk3Nzg3MzcifX0sInYiOjF9", "attenuation_hmac_inputs": [], "claims": { "org_id": "org_multisig", "user_id": "u_alice", - "realm": "w1", "agent_name": "coder", "run_id": "r_01h8alpharootinvocation00", "effective_caveats": { @@ -86,10 +78,12 @@ ], "max_cost_usd": 5, "max_steps": 100, + "budget": null, "exp": "2026-05-14T10:10:00Z" }, "ua_nonce": "9f4e1c8b2a3d4e5f6a7b8c9d0e1f2a3b", "ua_budget": null, + "permitted_realms": null, "nonces": [ "9f4e1c8b2a3d4e5f6a7b8c9d0e1f2a3b", "7c2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c" diff --git a/gateway/auth/fixtures/05-budget-envelope.json b/gateway/auth/fixtures/05-budget-envelope.json index d269c6100..d3176d26a 100644 --- a/gateway/auth/fixtures/05-budget-envelope.json +++ b/gateway/auth/fixtures/05-budget-envelope.json @@ -17,18 +17,12 @@ "alg": "ed25519", "key": "d04ab232742bb4ab3a1368bd4615e4e6d0224ab71a016baf8520a332c9778737" }, - "permissions": { - "realms": [ - "w1", - "w2" - ], - "agents": [ - "coder", - "browser", - "web-search", - "repair-agent" - ] - }, + "agents": [ + "coder", + "browser", + "web-search", + "repair-agent" + ], "iat": "2026-05-14T09:00:00Z", "exp": "2026-06-14T09:00:00Z", "nonce": "ab1234567890abcdef1234567890abcd", @@ -38,7 +32,6 @@ } }, "inv_unsigned": { - "realm": "w1", "agents": [ "coder" ], @@ -52,17 +45,16 @@ "atts_unsigned": [] }, "expected": { - "ua_signing_bytes_hex": "7b22627564676574223a7b226d61785f7065725f696e766f636174696f6e5f757364223a32352c226d61785f746f74616c5f757364223a313030307d2c22657870223a22323032362d30362d31345430393a30303a30305a222c22696174223a22323032362d30352d31345430393a30303a30305a222c226e6f6e6365223a226162313233343536373839306162636465663132333435363738393061626364222c227065726d697373696f6e73223a7b226167656e7473223a5b22636f646572222c2262726f77736572222c227765622d736561726368222c227265706169722d6167656e74225d2c227265616c6d73223a5b227731222c227732225d7d2c22757365725f6964223a22755f616c696365222c22757365725f7075626b6579223a7b22616c67223a2265643235353139222c226b6579223a2264303461623233323734326262346162336131333638626434363135653465366430323234616237316130313662616638353230613333326339373738373337227d7d", - "inv_signing_bytes_hex": "7b226167656e7473223a5b22636f646572225d2c22657870223a22323032362d30352d31345431303a31303a30305a222c22696174223a22323032362d30352d31345431303a30303a30305a222c226d61785f636f73745f757364223a352c226d61785f7374657073223a3130302c226e6f6e6365223a226364313233343536373839306162636465663132333435363738393061626364222c227265616c6d223a227731222c2272756e5f6964223a22725f30316838627564676574656e76656c6f706572756e30303030227d", - "ua_canonical_json": "{\"budget\":{\"max_per_invocation_usd\":25,\"max_total_usd\":1000},\"exp\":\"2026-06-14T09:00:00Z\",\"iat\":\"2026-05-14T09:00:00Z\",\"nonce\":\"ab1234567890abcdef1234567890abcd\",\"permissions\":{\"agents\":[\"coder\",\"browser\",\"web-search\",\"repair-agent\"],\"realms\":[\"w1\",\"w2\"]},\"user_id\":\"u_alice\",\"user_pubkey\":{\"alg\":\"ed25519\",\"key\":\"d04ab232742bb4ab3a1368bd4615e4e6d0224ab71a016baf8520a332c9778737\"}}", - "inv_canonical_json": "{\"agents\":[\"coder\"],\"exp\":\"2026-05-14T10:10:00Z\",\"iat\":\"2026-05-14T10:00:00Z\",\"max_cost_usd\":5,\"max_steps\":100,\"nonce\":\"cd1234567890abcdef1234567890abcd\",\"realm\":\"w1\",\"run_id\":\"r_01h8budgetenveloperun0000\"}", - "macaroon_canonical_json": "{\"attenuations\":[],\"invocation\":{\"agents\":[\"coder\"],\"exp\":\"2026-05-14T10:10:00Z\",\"iat\":\"2026-05-14T10:00:00Z\",\"max_cost_usd\":5,\"max_steps\":100,\"nonce\":\"cd1234567890abcdef1234567890abcd\",\"realm\":\"w1\",\"run_id\":\"r_01h8budgetenveloperun0000\",\"user_sig\":{\"alg\":\"ed25519\",\"sig\":\"840f1bc001ef31aefd0a94ba51a0c04484aa4b7060e120ff3de5e66684bd13ad839f423d7c594ac70b66125daf765097f31c0cf4620542755187b18ee6c14502\"}},\"org_id\":\"org_acme\",\"user_authorization\":{\"budget\":{\"max_per_invocation_usd\":25,\"max_total_usd\":1000},\"exp\":\"2026-06-14T09:00:00Z\",\"iat\":\"2026-05-14T09:00:00Z\",\"nonce\":\"ab1234567890abcdef1234567890abcd\",\"org_sig\":{\"alg\":\"ecdsa-secp256k1-sha256\",\"sig\":\"d4cac4a46ce2423033f84c76a28f1f0aab75fe36e5b1c660193a1a4e2283f2710903b79ba898647fd762d511124c5bd3b915f2f174011bdb06d731803e23d007\"},\"permissions\":{\"agents\":[\"coder\",\"browser\",\"web-search\",\"repair-agent\"],\"realms\":[\"w1\",\"w2\"]},\"user_id\":\"u_alice\",\"user_pubkey\":{\"alg\":\"ed25519\",\"key\":\"d04ab232742bb4ab3a1368bd4615e4e6d0224ab71a016baf8520a332c9778737\"}},\"v\":1}", - "macaroon_b64url": "eyJhdHRlbnVhdGlvbnMiOltdLCJpbnZvY2F0aW9uIjp7ImFnZW50cyI6WyJjb2RlciJdLCJleHAiOiIyMDI2LTA1LTE0VDEwOjEwOjAwWiIsImlhdCI6IjIwMjYtMDUtMTRUMTA6MDA6MDBaIiwibWF4X2Nvc3RfdXNkIjo1LCJtYXhfc3RlcHMiOjEwMCwibm9uY2UiOiJjZDEyMzQ1Njc4OTBhYmNkZWYxMjM0NTY3ODkwYWJjZCIsInJlYWxtIjoidzEiLCJydW5faWQiOiJyXzAxaDhidWRnZXRlbnZlbG9wZXJ1bjAwMDAiLCJ1c2VyX3NpZyI6eyJhbGciOiJlZDI1NTE5Iiwic2lnIjoiODQwZjFiYzAwMWVmMzFhZWZkMGE5NGJhNTFhMGMwNDQ4NGFhNGI3MDYwZTEyMGZmM2RlNWU2NjY4NGJkMTNhZDgzOWY0MjNkN2M1OTRhYzcwYjY2MTI1ZGFmNzY1MDk3ZjMxYzBjZjQ2MjA1NDI3NTUxODdiMThlZTZjMTQ1MDIifX0sIm9yZ19pZCI6Im9yZ19hY21lIiwidXNlcl9hdXRob3JpemF0aW9uIjp7ImJ1ZGdldCI6eyJtYXhfcGVyX2ludm9jYXRpb25fdXNkIjoyNSwibWF4X3RvdGFsX3VzZCI6MTAwMH0sImV4cCI6IjIwMjYtMDYtMTRUMDk6MDA6MDBaIiwiaWF0IjoiMjAyNi0wNS0xNFQwOTowMDowMFoiLCJub25jZSI6ImFiMTIzNDU2Nzg5MGFiY2RlZjEyMzQ1Njc4OTBhYmNkIiwib3JnX3NpZyI6eyJhbGciOiJlY2RzYS1zZWNwMjU2azEtc2hhMjU2Iiwic2lnIjoiZDRjYWM0YTQ2Y2UyNDIzMDMzZjg0Yzc2YTI4ZjFmMGFhYjc1ZmUzNmU1YjFjNjYwMTkzYTFhNGUyMjgzZjI3MTA5MDNiNzliYTg5ODY0N2ZkNzYyZDUxMTEyNGM1YmQzYjkxNWYyZjE3NDAxMWJkYjA2ZDczMTgwM2UyM2QwMDcifSwicGVybWlzc2lvbnMiOnsiYWdlbnRzIjpbImNvZGVyIiwiYnJvd3NlciIsIndlYi1zZWFyY2giLCJyZXBhaXItYWdlbnQiXSwicmVhbG1zIjpbIncxIiwidzIiXX0sInVzZXJfaWQiOiJ1X2FsaWNlIiwidXNlcl9wdWJrZXkiOnsiYWxnIjoiZWQyNTUxOSIsImtleSI6ImQwNGFiMjMyNzQyYmI0YWIzYTEzNjhiZDQ2MTVlNGU2ZDAyMjRhYjcxYTAxNmJhZjg1MjBhMzMyYzk3Nzg3MzcifX0sInYiOjF9", + "ua_signing_bytes_hex": "7b226167656e7473223a5b22636f646572222c2262726f77736572222c227765622d736561726368222c227265706169722d6167656e74225d2c22627564676574223a7b226d61785f7065725f696e766f636174696f6e5f757364223a32352c226d61785f746f74616c5f757364223a313030307d2c22657870223a22323032362d30362d31345430393a30303a30305a222c22696174223a22323032362d30352d31345430393a30303a30305a222c226e6f6e6365223a226162313233343536373839306162636465663132333435363738393061626364222c22757365725f6964223a22755f616c696365222c22757365725f7075626b6579223a7b22616c67223a2265643235353139222c226b6579223a2264303461623233323734326262346162336131333638626434363135653465366430323234616237316130313662616638353230613333326339373738373337227d7d", + "inv_signing_bytes_hex": "7b226167656e7473223a5b22636f646572225d2c22657870223a22323032362d30352d31345431303a31303a30305a222c22696174223a22323032362d30352d31345431303a30303a30305a222c226d61785f636f73745f757364223a352c226d61785f7374657073223a3130302c226e6f6e6365223a226364313233343536373839306162636465663132333435363738393061626364222c2272756e5f6964223a22725f30316838627564676574656e76656c6f706572756e30303030227d", + "ua_canonical_json": "{\"agents\":[\"coder\",\"browser\",\"web-search\",\"repair-agent\"],\"budget\":{\"max_per_invocation_usd\":25,\"max_total_usd\":1000},\"exp\":\"2026-06-14T09:00:00Z\",\"iat\":\"2026-05-14T09:00:00Z\",\"nonce\":\"ab1234567890abcdef1234567890abcd\",\"user_id\":\"u_alice\",\"user_pubkey\":{\"alg\":\"ed25519\",\"key\":\"d04ab232742bb4ab3a1368bd4615e4e6d0224ab71a016baf8520a332c9778737\"}}", + "inv_canonical_json": "{\"agents\":[\"coder\"],\"exp\":\"2026-05-14T10:10:00Z\",\"iat\":\"2026-05-14T10:00:00Z\",\"max_cost_usd\":5,\"max_steps\":100,\"nonce\":\"cd1234567890abcdef1234567890abcd\",\"run_id\":\"r_01h8budgetenveloperun0000\"}", + "macaroon_canonical_json": "{\"attenuations\":[],\"invocation\":{\"agents\":[\"coder\"],\"exp\":\"2026-05-14T10:10:00Z\",\"iat\":\"2026-05-14T10:00:00Z\",\"max_cost_usd\":5,\"max_steps\":100,\"nonce\":\"cd1234567890abcdef1234567890abcd\",\"run_id\":\"r_01h8budgetenveloperun0000\",\"user_sig\":{\"alg\":\"ed25519\",\"sig\":\"cea7ca9d4ea12badf733f2bd5478e6c5cff6f11a195af78f49e81f362b98139528855aa20ce68d87bcb4c6d6ef0241846ce219b965b63a5f71737eb0a49d6b01\"}},\"org_id\":\"org_acme\",\"user_authorization\":{\"agents\":[\"coder\",\"browser\",\"web-search\",\"repair-agent\"],\"budget\":{\"max_per_invocation_usd\":25,\"max_total_usd\":1000},\"exp\":\"2026-06-14T09:00:00Z\",\"iat\":\"2026-05-14T09:00:00Z\",\"nonce\":\"ab1234567890abcdef1234567890abcd\",\"org_sig\":{\"alg\":\"ecdsa-secp256k1-sha256\",\"sig\":\"376649daaf34d3d83367728ce338bc733c27045488f38c66dd3550c91011ba364868ef9c4a9b0cff22158f92ea2298c943d40e2b611c64a39f77f7cf42f1fea1\"},\"user_id\":\"u_alice\",\"user_pubkey\":{\"alg\":\"ed25519\",\"key\":\"d04ab232742bb4ab3a1368bd4615e4e6d0224ab71a016baf8520a332c9778737\"}},\"v\":1}", + "macaroon_b64url": "eyJhdHRlbnVhdGlvbnMiOltdLCJpbnZvY2F0aW9uIjp7ImFnZW50cyI6WyJjb2RlciJdLCJleHAiOiIyMDI2LTA1LTE0VDEwOjEwOjAwWiIsImlhdCI6IjIwMjYtMDUtMTRUMTA6MDA6MDBaIiwibWF4X2Nvc3RfdXNkIjo1LCJtYXhfc3RlcHMiOjEwMCwibm9uY2UiOiJjZDEyMzQ1Njc4OTBhYmNkZWYxMjM0NTY3ODkwYWJjZCIsInJ1bl9pZCI6InJfMDFoOGJ1ZGdldGVudmVsb3BlcnVuMDAwMCIsInVzZXJfc2lnIjp7ImFsZyI6ImVkMjU1MTkiLCJzaWciOiJjZWE3Y2E5ZDRlYTEyYmFkZjczM2YyYmQ1NDc4ZTZjNWNmZjZmMTFhMTk1YWY3OGY0OWU4MWYzNjJiOTgxMzk1Mjg4NTVhYTIwY2U2OGQ4N2JjYjRjNmQ2ZWYwMjQxODQ2Y2UyMTliOTY1YjYzYTVmNzE3MzdlYjBhNDlkNmIwMSJ9fSwib3JnX2lkIjoib3JnX2FjbWUiLCJ1c2VyX2F1dGhvcml6YXRpb24iOnsiYWdlbnRzIjpbImNvZGVyIiwiYnJvd3NlciIsIndlYi1zZWFyY2giLCJyZXBhaXItYWdlbnQiXSwiYnVkZ2V0Ijp7Im1heF9wZXJfaW52b2NhdGlvbl91c2QiOjI1LCJtYXhfdG90YWxfdXNkIjoxMDAwfSwiZXhwIjoiMjAyNi0wNi0xNFQwOTowMDowMFoiLCJpYXQiOiIyMDI2LTA1LTE0VDA5OjAwOjAwWiIsIm5vbmNlIjoiYWIxMjM0NTY3ODkwYWJjZGVmMTIzNDU2Nzg5MGFiY2QiLCJvcmdfc2lnIjp7ImFsZyI6ImVjZHNhLXNlY3AyNTZrMS1zaGEyNTYiLCJzaWciOiIzNzY2NDlkYWFmMzRkM2Q4MzM2NzcyOGNlMzM4YmM3MzNjMjcwNDU0ODhmMzhjNjZkZDM1NTBjOTEwMTFiYTM2NDg2OGVmOWM0YTliMGNmZjIyMTU4ZjkyZWEyMjk4Yzk0M2Q0MGUyYjYxMWM2NGEzOWY3N2Y3Y2Y0MmYxZmVhMSJ9LCJ1c2VyX2lkIjoidV9hbGljZSIsInVzZXJfcHVia2V5Ijp7ImFsZyI6ImVkMjU1MTkiLCJrZXkiOiJkMDRhYjIzMjc0MmJiNGFiM2ExMzY4YmQ0NjE1ZTRlNmQwMjI0YWI3MWEwMTZiYWY4NTIwYTMzMmM5Nzc4NzM3In19LCJ2IjoxfQ", "attenuation_hmac_inputs": [], "claims": { "org_id": "org_acme", "user_id": "u_alice", - "realm": "w1", "agent_name": "coder", "run_id": "r_01h8budgetenveloperun0000", "effective_caveats": { @@ -71,6 +63,10 @@ ], "max_cost_usd": 5, "max_steps": 100, + "budget": { + "max_total_usd": 1000, + "max_per_invocation_usd": 25 + }, "exp": "2026-05-14T10:10:00Z" }, "ua_nonce": "ab1234567890abcdef1234567890abcd", @@ -78,6 +74,7 @@ "max_total_usd": 1000, "max_per_invocation_usd": 25 }, + "permitted_realms": null, "nonces": [ "ab1234567890abcdef1234567890abcd", "cd1234567890abcdef1234567890abcd" diff --git a/gateway/auth/fixtures/06-multi-realm.json b/gateway/auth/fixtures/06-multi-realm.json new file mode 100644 index 000000000..041c71b1a --- /dev/null +++ b/gateway/auth/fixtures/06-multi-realm.json @@ -0,0 +1,121 @@ +{ + "description": "multi-realm UA + invocation: org grants per-realm caps {w1:$500, w2:$200}, invocation narrows to {w1:$5, w2:$2}; no attenuations", + "inputs": { + "org_id": "org_acme", + "org_priv_hex": "0000000000000000000000000000000000000000000000000000000000000001", + "user_priv_hex": "1111111111111111111111111111111111111111111111111111111111111111", + "policy": { + "type": "single", + "key": { + "alg": "ecdsa-secp256k1-sha256", + "key": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + } + }, + "ua_unsigned": { + "user_id": "u_alice", + "user_pubkey": { + "alg": "ed25519", + "key": "d04ab232742bb4ab3a1368bd4615e4e6d0224ab71a016baf8520a332c9778737" + }, + "agents": [ + "coder", + "browser", + "web-search", + "repair-agent" + ], + "iat": "2026-05-14T09:00:00Z", + "exp": "2026-06-14T09:00:00Z", + "nonce": "f06e1d2c3b4a5968778899aabbccddee", + "budget": { + "max_total_usd": 1000, + "max_per_invocation_usd": 25, + "realm_budgets": { + "w1": { + "max_total_usd": 500 + }, + "w2": { + "max_total_usd": 200 + } + } + } + }, + "inv_unsigned": { + "agents": [ + "coder" + ], + "run_id": "r_01h8multirealmrun000000000", + "max_cost_usd": 5, + "max_steps": 100, + "iat": "2026-05-14T10:00:00Z", + "exp": "2026-05-14T10:10:00Z", + "nonce": "f16e1d2c3b4a5968778899aabbccddee", + "budget": { + "realm_budgets": { + "w1": { + "max_total_usd": 5 + }, + "w2": { + "max_total_usd": 2 + } + } + } + }, + "atts_unsigned": [] + }, + "expected": { + "ua_signing_bytes_hex": "7b226167656e7473223a5b22636f646572222c2262726f77736572222c227765622d736561726368222c227265706169722d6167656e74225d2c22627564676574223a7b226d61785f7065725f696e766f636174696f6e5f757364223a32352c226d61785f746f74616c5f757364223a313030302c227265616c6d5f62756467657473223a7b227731223a7b226d61785f746f74616c5f757364223a3530307d2c227732223a7b226d61785f746f74616c5f757364223a3230307d7d7d2c22657870223a22323032362d30362d31345430393a30303a30305a222c22696174223a22323032362d30352d31345430393a30303a30305a222c226e6f6e6365223a226630366531643263336234613539363837373838393961616262636364646565222c22757365725f6964223a22755f616c696365222c22757365725f7075626b6579223a7b22616c67223a2265643235353139222c226b6579223a2264303461623233323734326262346162336131333638626434363135653465366430323234616237316130313662616638353230613333326339373738373337227d7d", + "inv_signing_bytes_hex": "7b226167656e7473223a5b22636f646572225d2c22627564676574223a7b227265616c6d5f62756467657473223a7b227731223a7b226d61785f746f74616c5f757364223a357d2c227732223a7b226d61785f746f74616c5f757364223a327d7d7d2c22657870223a22323032362d30352d31345431303a31303a30305a222c22696174223a22323032362d30352d31345431303a30303a30305a222c226d61785f636f73745f757364223a352c226d61785f7374657073223a3130302c226e6f6e6365223a226631366531643263336234613539363837373838393961616262636364646565222c2272756e5f6964223a22725f303168386d756c74697265616c6d72756e303030303030303030227d", + "ua_canonical_json": "{\"agents\":[\"coder\",\"browser\",\"web-search\",\"repair-agent\"],\"budget\":{\"max_per_invocation_usd\":25,\"max_total_usd\":1000,\"realm_budgets\":{\"w1\":{\"max_total_usd\":500},\"w2\":{\"max_total_usd\":200}}},\"exp\":\"2026-06-14T09:00:00Z\",\"iat\":\"2026-05-14T09:00:00Z\",\"nonce\":\"f06e1d2c3b4a5968778899aabbccddee\",\"user_id\":\"u_alice\",\"user_pubkey\":{\"alg\":\"ed25519\",\"key\":\"d04ab232742bb4ab3a1368bd4615e4e6d0224ab71a016baf8520a332c9778737\"}}", + "inv_canonical_json": "{\"agents\":[\"coder\"],\"budget\":{\"realm_budgets\":{\"w1\":{\"max_total_usd\":5},\"w2\":{\"max_total_usd\":2}}},\"exp\":\"2026-05-14T10:10:00Z\",\"iat\":\"2026-05-14T10:00:00Z\",\"max_cost_usd\":5,\"max_steps\":100,\"nonce\":\"f16e1d2c3b4a5968778899aabbccddee\",\"run_id\":\"r_01h8multirealmrun000000000\"}", + "macaroon_canonical_json": "{\"attenuations\":[],\"invocation\":{\"agents\":[\"coder\"],\"budget\":{\"realm_budgets\":{\"w1\":{\"max_total_usd\":5},\"w2\":{\"max_total_usd\":2}}},\"exp\":\"2026-05-14T10:10:00Z\",\"iat\":\"2026-05-14T10:00:00Z\",\"max_cost_usd\":5,\"max_steps\":100,\"nonce\":\"f16e1d2c3b4a5968778899aabbccddee\",\"run_id\":\"r_01h8multirealmrun000000000\",\"user_sig\":{\"alg\":\"ed25519\",\"sig\":\"9e4cec9fbc3dde2a432f47b22135070b7d1b6dc253cbf74f03f1b06e6cb7a353058c6a6c85c4dc011ece4fee9f77970d99b6cd4cb3a515e06ef9009849062c0f\"}},\"org_id\":\"org_acme\",\"user_authorization\":{\"agents\":[\"coder\",\"browser\",\"web-search\",\"repair-agent\"],\"budget\":{\"max_per_invocation_usd\":25,\"max_total_usd\":1000,\"realm_budgets\":{\"w1\":{\"max_total_usd\":500},\"w2\":{\"max_total_usd\":200}}},\"exp\":\"2026-06-14T09:00:00Z\",\"iat\":\"2026-05-14T09:00:00Z\",\"nonce\":\"f06e1d2c3b4a5968778899aabbccddee\",\"org_sig\":{\"alg\":\"ecdsa-secp256k1-sha256\",\"sig\":\"9fb3898276eebdc3e622d532aff6a2c33ba82b9fa137b3e770253c550863ac2f2828fb165c87b7c51f810b95430133025902fb1cabea1f6158700931b00f9f91\"},\"user_id\":\"u_alice\",\"user_pubkey\":{\"alg\":\"ed25519\",\"key\":\"d04ab232742bb4ab3a1368bd4615e4e6d0224ab71a016baf8520a332c9778737\"}},\"v\":1}", + "macaroon_b64url": "eyJhdHRlbnVhdGlvbnMiOltdLCJpbnZvY2F0aW9uIjp7ImFnZW50cyI6WyJjb2RlciJdLCJidWRnZXQiOnsicmVhbG1fYnVkZ2V0cyI6eyJ3MSI6eyJtYXhfdG90YWxfdXNkIjo1fSwidzIiOnsibWF4X3RvdGFsX3VzZCI6Mn19fSwiZXhwIjoiMjAyNi0wNS0xNFQxMDoxMDowMFoiLCJpYXQiOiIyMDI2LTA1LTE0VDEwOjAwOjAwWiIsIm1heF9jb3N0X3VzZCI6NSwibWF4X3N0ZXBzIjoxMDAsIm5vbmNlIjoiZjE2ZTFkMmMzYjRhNTk2ODc3ODg5OWFhYmJjY2RkZWUiLCJydW5faWQiOiJyXzAxaDhtdWx0aXJlYWxtcnVuMDAwMDAwMDAwIiwidXNlcl9zaWciOnsiYWxnIjoiZWQyNTUxOSIsInNpZyI6IjllNGNlYzlmYmMzZGRlMmE0MzJmNDdiMjIxMzUwNzBiN2QxYjZkYzI1M2NiZjc0ZjAzZjFiMDZlNmNiN2EzNTMwNThjNmE2Yzg1YzRkYzAxMWVjZTRmZWU5Zjc3OTcwZDk5YjZjZDRjYjNhNTE1ZTA2ZWY5MDA5ODQ5MDYyYzBmIn19LCJvcmdfaWQiOiJvcmdfYWNtZSIsInVzZXJfYXV0aG9yaXphdGlvbiI6eyJhZ2VudHMiOlsiY29kZXIiLCJicm93c2VyIiwid2ViLXNlYXJjaCIsInJlcGFpci1hZ2VudCJdLCJidWRnZXQiOnsibWF4X3Blcl9pbnZvY2F0aW9uX3VzZCI6MjUsIm1heF90b3RhbF91c2QiOjEwMDAsInJlYWxtX2J1ZGdldHMiOnsidzEiOnsibWF4X3RvdGFsX3VzZCI6NTAwfSwidzIiOnsibWF4X3RvdGFsX3VzZCI6MjAwfX19LCJleHAiOiIyMDI2LTA2LTE0VDA5OjAwOjAwWiIsImlhdCI6IjIwMjYtMDUtMTRUMDk6MDA6MDBaIiwibm9uY2UiOiJmMDZlMWQyYzNiNGE1OTY4Nzc4ODk5YWFiYmNjZGRlZSIsIm9yZ19zaWciOnsiYWxnIjoiZWNkc2Etc2VjcDI1NmsxLXNoYTI1NiIsInNpZyI6IjlmYjM4OTgyNzZlZWJkYzNlNjIyZDUzMmFmZjZhMmMzM2JhODJiOWZhMTM3YjNlNzcwMjUzYzU1MDg2M2FjMmYyODI4ZmIxNjVjODdiN2M1MWY4MTBiOTU0MzAxMzMwMjU5MDJmYjFjYWJlYTFmNjE1ODcwMDkzMWIwMGY5ZjkxIn0sInVzZXJfaWQiOiJ1X2FsaWNlIiwidXNlcl9wdWJrZXkiOnsiYWxnIjoiZWQyNTUxOSIsImtleSI6ImQwNGFiMjMyNzQyYmI0YWIzYTEzNjhiZDQ2MTVlNGU2ZDAyMjRhYjcxYTAxNmJhZjg1MjBhMzMyYzk3Nzg3MzcifX0sInYiOjF9", + "attenuation_hmac_inputs": [], + "claims": { + "org_id": "org_acme", + "user_id": "u_alice", + "agent_name": "coder", + "run_id": "r_01h8multirealmrun000000000", + "effective_caveats": { + "agents": [ + "coder" + ], + "max_cost_usd": 5, + "max_steps": 100, + "budget": { + "realm_budgets": { + "w1": { + "max_total_usd": 5 + }, + "w2": { + "max_total_usd": 2 + } + } + }, + "exp": "2026-05-14T10:10:00Z" + }, + "ua_nonce": "f06e1d2c3b4a5968778899aabbccddee", + "ua_budget": { + "max_total_usd": 1000, + "max_per_invocation_usd": 25, + "realm_budgets": { + "w1": { + "max_total_usd": 500 + }, + "w2": { + "max_total_usd": 200 + } + } + }, + "permitted_realms": [ + "w1", + "w2" + ], + "nonces": [ + "f06e1d2c3b4a5968778899aabbccddee", + "f16e1d2c3b4a5968778899aabbccddee" + ], + "iat": "2026-05-14T10:00:00Z" + }, + "verify_result": "ok" + } +} diff --git a/gateway/auth/fixtures/07-cross-realm-attenuation.json b/gateway/auth/fixtures/07-cross-realm-attenuation.json new file mode 100644 index 000000000..e307cc92d --- /dev/null +++ b/gateway/auth/fixtures/07-cross-realm-attenuation.json @@ -0,0 +1,145 @@ +{ + "description": "cross-realm sub-agent attenuation: parent invocation allows w1+w2, child attenuates to w2 only with a smaller cap (HMAC-chained, no issuer round-trip)", + "inputs": { + "org_id": "org_acme", + "org_priv_hex": "0000000000000000000000000000000000000000000000000000000000000001", + "user_priv_hex": "1111111111111111111111111111111111111111111111111111111111111111", + "policy": { + "type": "single", + "key": { + "alg": "ecdsa-secp256k1-sha256", + "key": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + } + }, + "ua_unsigned": { + "user_id": "u_alice", + "user_pubkey": { + "alg": "ed25519", + "key": "d04ab232742bb4ab3a1368bd4615e4e6d0224ab71a016baf8520a332c9778737" + }, + "agents": [ + "coder", + "browser", + "web-search", + "repair-agent" + ], + "iat": "2026-05-14T09:00:00Z", + "exp": "2026-06-14T09:00:00Z", + "nonce": "07e1d2c3b4a596778899aabbccddee01", + "budget": { + "max_total_usd": 1000, + "max_per_invocation_usd": 25, + "realm_budgets": { + "w1": { + "max_total_usd": 500 + }, + "w2": { + "max_total_usd": 200 + } + } + } + }, + "inv_unsigned": { + "agents": [ + "coder" + ], + "run_id": "r_01h8crossrealmparent00000", + "max_cost_usd": 5, + "max_steps": 100, + "iat": "2026-05-14T10:00:00Z", + "exp": "2026-05-14T10:10:00Z", + "nonce": "07e1d2c3b4a596778899aabbccddee02", + "budget": { + "realm_budgets": { + "w1": { + "max_total_usd": 5 + }, + "w2": { + "max_total_usd": 4 + } + } + } + }, + "atts_unsigned": [ + { + "agents": [ + "coder", + "web-search" + ], + "max_cost_usd": 2, + "max_steps": 40, + "run_id": "r_01h8crossrealmsubchild00", + "exp": "2026-05-14T10:02:00Z", + "nonce": "07e1d2c3b4a596778899aabbccddee03", + "budget": { + "realm_budgets": { + "w2": { + "max_total_usd": 1 + } + } + } + } + ] + }, + "expected": { + "ua_signing_bytes_hex": "7b226167656e7473223a5b22636f646572222c2262726f77736572222c227765622d736561726368222c227265706169722d6167656e74225d2c22627564676574223a7b226d61785f7065725f696e766f636174696f6e5f757364223a32352c226d61785f746f74616c5f757364223a313030302c227265616c6d5f62756467657473223a7b227731223a7b226d61785f746f74616c5f757364223a3530307d2c227732223a7b226d61785f746f74616c5f757364223a3230307d7d7d2c22657870223a22323032362d30362d31345430393a30303a30305a222c22696174223a22323032362d30352d31345430393a30303a30305a222c226e6f6e6365223a223037653164326333623461353936373738383939616162626363646465653031222c22757365725f6964223a22755f616c696365222c22757365725f7075626b6579223a7b22616c67223a2265643235353139222c226b6579223a2264303461623233323734326262346162336131333638626434363135653465366430323234616237316130313662616638353230613333326339373738373337227d7d", + "inv_signing_bytes_hex": "7b226167656e7473223a5b22636f646572225d2c22627564676574223a7b227265616c6d5f62756467657473223a7b227731223a7b226d61785f746f74616c5f757364223a357d2c227732223a7b226d61785f746f74616c5f757364223a347d7d7d2c22657870223a22323032362d30352d31345431303a31303a30305a222c22696174223a22323032362d30352d31345431303a30303a30305a222c226d61785f636f73745f757364223a352c226d61785f7374657073223a3130302c226e6f6e6365223a223037653164326333623461353936373738383939616162626363646465653032222c2272756e5f6964223a22725f3031683863726f73737265616c6d706172656e743030303030227d", + "ua_canonical_json": "{\"agents\":[\"coder\",\"browser\",\"web-search\",\"repair-agent\"],\"budget\":{\"max_per_invocation_usd\":25,\"max_total_usd\":1000,\"realm_budgets\":{\"w1\":{\"max_total_usd\":500},\"w2\":{\"max_total_usd\":200}}},\"exp\":\"2026-06-14T09:00:00Z\",\"iat\":\"2026-05-14T09:00:00Z\",\"nonce\":\"07e1d2c3b4a596778899aabbccddee01\",\"user_id\":\"u_alice\",\"user_pubkey\":{\"alg\":\"ed25519\",\"key\":\"d04ab232742bb4ab3a1368bd4615e4e6d0224ab71a016baf8520a332c9778737\"}}", + "inv_canonical_json": "{\"agents\":[\"coder\"],\"budget\":{\"realm_budgets\":{\"w1\":{\"max_total_usd\":5},\"w2\":{\"max_total_usd\":4}}},\"exp\":\"2026-05-14T10:10:00Z\",\"iat\":\"2026-05-14T10:00:00Z\",\"max_cost_usd\":5,\"max_steps\":100,\"nonce\":\"07e1d2c3b4a596778899aabbccddee02\",\"run_id\":\"r_01h8crossrealmparent00000\"}", + "macaroon_canonical_json": "{\"attenuations\":[{\"caveats\":{\"agents\":[\"coder\",\"web-search\"],\"budget\":{\"realm_budgets\":{\"w2\":{\"max_total_usd\":1}}},\"exp\":\"2026-05-14T10:02:00Z\",\"max_cost_usd\":2,\"max_steps\":40,\"nonce\":\"07e1d2c3b4a596778899aabbccddee03\",\"run_id\":\"r_01h8crossrealmsubchild00\"},\"hmac\":\"c1ca8e4046c637561f5c61e741f202d65c22bd3999a590c4fcf94bb0d619bdf0\"}],\"invocation\":{\"agents\":[\"coder\"],\"budget\":{\"realm_budgets\":{\"w1\":{\"max_total_usd\":5},\"w2\":{\"max_total_usd\":4}}},\"exp\":\"2026-05-14T10:10:00Z\",\"iat\":\"2026-05-14T10:00:00Z\",\"max_cost_usd\":5,\"max_steps\":100,\"nonce\":\"07e1d2c3b4a596778899aabbccddee02\",\"run_id\":\"r_01h8crossrealmparent00000\",\"user_sig\":{\"alg\":\"ed25519\",\"sig\":\"810c365a81713a3fea2e2033f69aa258b18e1e3c70e994eff7c43d4094956a20d637db6856bb7f557a44bc888cfa3969670d250d60d5da6cb826ff3fcf1ce50e\"}},\"org_id\":\"org_acme\",\"user_authorization\":{\"agents\":[\"coder\",\"browser\",\"web-search\",\"repair-agent\"],\"budget\":{\"max_per_invocation_usd\":25,\"max_total_usd\":1000,\"realm_budgets\":{\"w1\":{\"max_total_usd\":500},\"w2\":{\"max_total_usd\":200}}},\"exp\":\"2026-06-14T09:00:00Z\",\"iat\":\"2026-05-14T09:00:00Z\",\"nonce\":\"07e1d2c3b4a596778899aabbccddee01\",\"org_sig\":{\"alg\":\"ecdsa-secp256k1-sha256\",\"sig\":\"1d7fb559c30afce1ab7b0ff2b0c42c4093e28aaea08f181c39694cb109860fee63273ede0d2794d2e944539dc0698509d78249225c23ea5f3cfe67c719cf1853\"},\"user_id\":\"u_alice\",\"user_pubkey\":{\"alg\":\"ed25519\",\"key\":\"d04ab232742bb4ab3a1368bd4615e4e6d0224ab71a016baf8520a332c9778737\"}},\"v\":1}", + "macaroon_b64url": "eyJhdHRlbnVhdGlvbnMiOlt7ImNhdmVhdHMiOnsiYWdlbnRzIjpbImNvZGVyIiwid2ViLXNlYXJjaCJdLCJidWRnZXQiOnsicmVhbG1fYnVkZ2V0cyI6eyJ3MiI6eyJtYXhfdG90YWxfdXNkIjoxfX19LCJleHAiOiIyMDI2LTA1LTE0VDEwOjAyOjAwWiIsIm1heF9jb3N0X3VzZCI6MiwibWF4X3N0ZXBzIjo0MCwibm9uY2UiOiIwN2UxZDJjM2I0YTU5Njc3ODg5OWFhYmJjY2RkZWUwMyIsInJ1bl9pZCI6InJfMDFoOGNyb3NzcmVhbG1zdWJjaGlsZDAwIn0sImhtYWMiOiJjMWNhOGU0MDQ2YzYzNzU2MWY1YzYxZTc0MWYyMDJkNjVjMjJiZDM5OTlhNTkwYzRmY2Y5NGJiMGQ2MTliZGYwIn1dLCJpbnZvY2F0aW9uIjp7ImFnZW50cyI6WyJjb2RlciJdLCJidWRnZXQiOnsicmVhbG1fYnVkZ2V0cyI6eyJ3MSI6eyJtYXhfdG90YWxfdXNkIjo1fSwidzIiOnsibWF4X3RvdGFsX3VzZCI6NH19fSwiZXhwIjoiMjAyNi0wNS0xNFQxMDoxMDowMFoiLCJpYXQiOiIyMDI2LTA1LTE0VDEwOjAwOjAwWiIsIm1heF9jb3N0X3VzZCI6NSwibWF4X3N0ZXBzIjoxMDAsIm5vbmNlIjoiMDdlMWQyYzNiNGE1OTY3Nzg4OTlhYWJiY2NkZGVlMDIiLCJydW5faWQiOiJyXzAxaDhjcm9zc3JlYWxtcGFyZW50MDAwMDAiLCJ1c2VyX3NpZyI6eyJhbGciOiJlZDI1NTE5Iiwic2lnIjoiODEwYzM2NWE4MTcxM2EzZmVhMmUyMDMzZjY5YWEyNThiMThlMWUzYzcwZTk5NGVmZjdjNDNkNDA5NDk1NmEyMGQ2MzdkYjY4NTZiYjdmNTU3YTQ0YmM4ODhjZmEzOTY5NjcwZDI1MGQ2MGQ1ZGE2Y2I4MjZmZjNmY2YxY2U1MGUifX0sIm9yZ19pZCI6Im9yZ19hY21lIiwidXNlcl9hdXRob3JpemF0aW9uIjp7ImFnZW50cyI6WyJjb2RlciIsImJyb3dzZXIiLCJ3ZWItc2VhcmNoIiwicmVwYWlyLWFnZW50Il0sImJ1ZGdldCI6eyJtYXhfcGVyX2ludm9jYXRpb25fdXNkIjoyNSwibWF4X3RvdGFsX3VzZCI6MTAwMCwicmVhbG1fYnVkZ2V0cyI6eyJ3MSI6eyJtYXhfdG90YWxfdXNkIjo1MDB9LCJ3MiI6eyJtYXhfdG90YWxfdXNkIjoyMDB9fX0sImV4cCI6IjIwMjYtMDYtMTRUMDk6MDA6MDBaIiwiaWF0IjoiMjAyNi0wNS0xNFQwOTowMDowMFoiLCJub25jZSI6IjA3ZTFkMmMzYjRhNTk2Nzc4ODk5YWFiYmNjZGRlZTAxIiwib3JnX3NpZyI6eyJhbGciOiJlY2RzYS1zZWNwMjU2azEtc2hhMjU2Iiwic2lnIjoiMWQ3ZmI1NTljMzBhZmNlMWFiN2IwZmYyYjBjNDJjNDA5M2UyOGFhZWEwOGYxODFjMzk2OTRjYjEwOTg2MGZlZTYzMjczZWRlMGQyNzk0ZDJlOTQ0NTM5ZGMwNjk4NTA5ZDc4MjQ5MjI1YzIzZWE1ZjNjZmU2N2M3MTljZjE4NTMifSwidXNlcl9pZCI6InVfYWxpY2UiLCJ1c2VyX3B1YmtleSI6eyJhbGciOiJlZDI1NTE5Iiwia2V5IjoiZDA0YWIyMzI3NDJiYjRhYjNhMTM2OGJkNDYxNWU0ZTZkMDIyNGFiNzFhMDE2YmFmODUyMGEzMzJjOTc3ODczNyJ9fSwidiI6MX0", + "attenuation_hmac_inputs": [ + { + "prev_sig_hex": "810c365a81713a3fea2e2033f69aa258b18e1e3c70e994eff7c43d4094956a20d637db6856bb7f557a44bc888cfa3969670d250d60d5da6cb826ff3fcf1ce50e", + "caveats_canonical_json": "{\"agents\":[\"coder\",\"web-search\"],\"budget\":{\"realm_budgets\":{\"w2\":{\"max_total_usd\":1}}},\"exp\":\"2026-05-14T10:02:00Z\",\"max_cost_usd\":2,\"max_steps\":40,\"nonce\":\"07e1d2c3b4a596778899aabbccddee03\",\"run_id\":\"r_01h8crossrealmsubchild00\"}", + "hmac_input_hex": "7b226167656e7473223a5b22636f646572222c227765622d736561726368225d2c22627564676574223a7b227265616c6d5f62756467657473223a7b227732223a7b226d61785f746f74616c5f757364223a317d7d7d2c22657870223a22323032362d30352d31345431303a30323a30305a222c226d61785f636f73745f757364223a322c226d61785f7374657073223a34302c226e6f6e6365223a223037653164326333623461353936373738383939616162626363646465653033222c2272756e5f6964223a22725f3031683863726f73737265616c6d7375626368696c643030227d", + "hmac_output_hex": "c1ca8e4046c637561f5c61e741f202d65c22bd3999a590c4fcf94bb0d619bdf0" + } + ], + "claims": { + "org_id": "org_acme", + "user_id": "u_alice", + "agent_name": "web-search", + "run_id": "r_01h8crossrealmsubchild00", + "effective_caveats": { + "agents": [ + "coder", + "web-search" + ], + "max_cost_usd": 2, + "max_steps": 40, + "budget": { + "realm_budgets": { + "w2": { + "max_total_usd": 1 + } + } + }, + "exp": "2026-05-14T10:02:00Z" + }, + "ua_nonce": "07e1d2c3b4a596778899aabbccddee01", + "ua_budget": { + "max_total_usd": 1000, + "max_per_invocation_usd": 25, + "realm_budgets": { + "w1": { + "max_total_usd": 500 + }, + "w2": { + "max_total_usd": 200 + } + } + }, + "permitted_realms": [ + "w2" + ], + "nonces": [ + "07e1d2c3b4a596778899aabbccddee01", + "07e1d2c3b4a596778899aabbccddee02", + "07e1d2c3b4a596778899aabbccddee03" + ], + "iat": "2026-05-14T10:00:00Z" + }, + "verify_result": "ok" + } +} diff --git a/gateway/auth/go/budget_test.go b/gateway/auth/go/budget_test.go index 8b3379fc3..f3855eb79 100644 --- a/gateway/auth/go/budget_test.go +++ b/gateway/auth/go/budget_test.go @@ -67,7 +67,7 @@ func mustHex32(s string) []byte { // one realm, one agent, no attenuations. Times are wall-clock so // "now" passed to Verify can be the same time.Now we used to set // iat/exp. -func buildVerifiableMacaroon(t *testing.T, budget *macaroon.UserBudget, invMaxCostUSD float64) (string, macaroon.Policy, time.Time) { +func buildVerifiableMacaroon(t *testing.T, budget *macaroon.Budget, invMaxCostUSD float64) (string, macaroon.Policy, time.Time) { t.Helper() orgPub, err := macaroon.EcdsaSecp256k1PublicKey(budgetOrgPriv) @@ -87,13 +87,10 @@ func buildVerifiableMacaroon(t *testing.T, budget *macaroon.UserBudget, invMaxCo ua := macaroon.UserAuthorization{ UserID: "u_alice", UserPubkey: macaroon.PubKey{Alg: macaroon.AlgEd25519, Key: macaroon.BytesToHex(userPub)}, - Permissions: macaroon.UserPermissions{ - Realms: []string{"w1"}, - Agents: []string{"coder"}, - }, - Budget: budget, - IAT: iat, - Exp: uaExp, + Agents: []string{"coder"}, + Budget: budget, + IAT: iat, + Exp: uaExp, // 32 hex chars (128-bit nonce) — fixed because nothing in // these tests cares about uniqueness. Nonce: "9f4e000000000000000000000000abcd", @@ -104,7 +101,6 @@ func buildVerifiableMacaroon(t *testing.T, budget *macaroon.UserBudget, invMaxCo } inv := macaroon.Invocation{ - Realm: "w1", Agents: []string{"coder"}, RunID: "r_budget_test", MaxCostUSD: invMaxCostUSD, @@ -153,7 +149,7 @@ func TestBudget_Absent_VerifiesAsBefore(t *testing.T) { } func TestBudget_WithinPerInvocationCap_Verifies(t *testing.T) { - budget := &macaroon.UserBudget{ + budget := &macaroon.Budget{ MaxTotalUSD: 1000, MaxPerInvocationUSD: 25, } @@ -174,7 +170,7 @@ func TestBudget_WithinPerInvocationCap_Verifies(t *testing.T) { } func TestBudget_ExceedsPerInvocationCap_Rejected(t *testing.T) { - budget := &macaroon.UserBudget{ + budget := &macaroon.Budget{ MaxTotalUSD: 1000, MaxPerInvocationUSD: 25, } @@ -197,7 +193,7 @@ func TestBudget_ZeroPerInvocationDisablesCap(t *testing.T) { // MaxPerInvocationUSD=0 means "no per-call cap" (only the // cumulative MaxTotalUSD applies, and that's adapter-enforced). // An invocation with a large MaxCostUSD must still verify. - budget := &macaroon.UserBudget{ + budget := &macaroon.Budget{ MaxTotalUSD: 1000, MaxPerInvocationUSD: 0, // explicit "no cap" } @@ -210,7 +206,7 @@ func TestBudget_ZeroPerInvocationDisablesCap(t *testing.T) { func TestBudget_ExactlyAtCap_Verifies(t *testing.T) { // Boundary: invocation == cap should be allowed. Only strictly // greater is rejected (matches the > comparison in verify.go). - budget := &macaroon.UserBudget{MaxPerInvocationUSD: 25} + budget := &macaroon.Budget{MaxPerInvocationUSD: 25} encoded, policy, now := buildVerifiableMacaroon(t, budget, 25.00) if _, err := macaroon.Verify(encoded, policy, now); err != nil { t.Fatalf("at-cap should be allowed: %v", err) diff --git a/gateway/auth/go/fixtures_test.go b/gateway/auth/go/fixtures_test.go index 892d3f4f5..cc05abe36 100644 --- a/gateway/auth/go/fixtures_test.go +++ b/gateway/auth/go/fixtures_test.go @@ -67,19 +67,20 @@ type fixtureFile struct { Claims struct { OrgID string `json:"org_id"` UserID string `json:"user_id"` - Realm string `json:"realm"` AgentName string `json:"agent_name"` RunID string `json:"run_id"` EffectiveCaveats struct { - Agents []string `json:"agents"` - MaxCostUSD float64 `json:"max_cost_usd"` - MaxSteps int `json:"max_steps"` - Exp string `json:"exp"` + Agents []string `json:"agents"` + MaxCostUSD float64 `json:"max_cost_usd"` + MaxSteps int `json:"max_steps"` + Budget *macaroon.Budget `json:"budget"` + Exp string `json:"exp"` } `json:"effective_caveats"` - UANonce string `json:"ua_nonce"` - UABudget *macaroon.UserBudget `json:"ua_budget"` - Nonces []string `json:"nonces"` - IAT string `json:"iat"` + UANonce string `json:"ua_nonce"` + UABudget *macaroon.Budget `json:"ua_budget"` + PermittedRealms []string `json:"permitted_realms"` + Nonces []string `json:"nonces"` + IAT string `json:"iat"` } `json:"claims"` VerifyResult string `json:"verify_result"` } `json:"expected"` @@ -223,9 +224,6 @@ func runFixture(t *testing.T, name string) { if claims.UserID != fx.Expected.Claims.UserID { t.Errorf("claims.UserID: got %q want %q", claims.UserID, fx.Expected.Claims.UserID) } - if claims.Realm != fx.Expected.Claims.Realm { - t.Errorf("claims.Realm: got %q want %q", claims.Realm, fx.Expected.Claims.Realm) - } if claims.AgentName != fx.Expected.Claims.AgentName { t.Errorf("claims.AgentName: got %q want %q", claims.AgentName, fx.Expected.Claims.AgentName) } @@ -260,6 +258,24 @@ func runFixture(t *testing.T, name string) { if !reflect.DeepEqual(claims.UABudget, fx.Expected.Claims.UABudget) { t.Errorf("claims.UABudget: got %+v want %+v", claims.UABudget, fx.Expected.Claims.UABudget) } + if !reflect.DeepEqual(claims.EffectiveCaveats.Budget, fx.Expected.Claims.EffectiveCaveats.Budget) { + t.Errorf("claims.EffectiveCaveats.Budget: got %+v want %+v", + claims.EffectiveCaveats.Budget, fx.Expected.Claims.EffectiveCaveats.Budget) + } + if !equalPermittedRealms(claims.PermittedRealms, fx.Expected.Claims.PermittedRealms) { + t.Errorf("claims.PermittedRealms: got %v want %v", + claims.PermittedRealms, fx.Expected.Claims.PermittedRealms) + } +} + +// equalPermittedRealms handles the fixture-vs-claims nil/empty +// distinction. Fixture JSON encodes null when there are no realms; +// Go decodes that to nil. Both sides agree on "no realms" → nil. +func equalPermittedRealms(got, want []string) bool { + if len(got) == 0 && len(want) == 0 { + return true + } + return reflect.DeepEqual(got, want) } // jcsStripFromRaw runs JCS on raw JSON. If field is non-empty, that diff --git a/gateway/auth/go/types.go b/gateway/auth/go/types.go index 4de29944e..0364e7a58 100644 --- a/gateway/auth/go/types.go +++ b/gateway/auth/go/types.go @@ -1,7 +1,9 @@ package macaroon // Wire types for the three-layer macaroon. Field names and JSON tags -// match gateway/plans/phases/phase-4-macaroon-shape.md exactly. +// match gateway/plans/phases/phase-4-macaroon-shape.md and the +// symmetric-recursive refinement in +// gateway/plans/phases/phase-11-symmetric-recursive-authorization.md. // // All binary fields (pubkeys, signatures, HMACs, nonces) are // lowercase hex strings WITHOUT 0x prefix. All timestamps are RFC @@ -67,62 +69,82 @@ type Policy struct { // ─── macaroon layers ────────────────────────────────────────────────── -// UserPermissions is what the org grants the user. Verifier enforces -// invocation.realm ∈ realms and inv.agents ⊆ agents. -type UserPermissions struct { - Realms []string `json:"realms"` - Agents []string `json:"agents"` +// RealmBudget is a per-realm spending cap inside a Budget. Phase 11 +// adds the realm-budgets map so multi-swarm deployments can pin +// exact per-swarm cumulative caps without relying on the implicit +// "max_total_usd leaks across swarms" behavior. +// +// Only MaxTotalUSD is defined today. Per-realm step / rate caps are +// an open question in phase 11 (see "Open questions" §2) and can be +// added without breaking compatibility — RealmBudget is a struct, +// not a bare number, precisely to leave that door open. +type RealmBudget struct { + MaxTotalUSD float64 `json:"max_total_usd"` } -// UserBudget is the org-signed spending envelope for a -// user_authorization. See phase-4-macaroon-shape.md ("Budget envelope") -// for the motivating cold-storage flow: org leader signs a UA with a -// weekly cap, employee's hot key signs many invocations under it. +// Budget is the spending envelope carried by any signed layer. +// Phase 11 renamed UserBudget→Budget so the same shape appears on +// UserAuthorization, Invocation, and AttenuationCaveats. Children +// narrow against parents using the rules in verify.go's narrow(). // -// Both fields are independently optional. Zero means "no cap on this -// axis" — consistent with the empty-by-default convention used for -// agent_budgets in phase 6. +// All fields are independently optional. Zero / nil means "no cap on +// this axis" — consistent with the empty-by-default convention used +// for agent_budgets in phase 6. The verifier checks structural +// narrowing (child ≤ parent at each axis); the adapter enforces the +// cumulative caps (MaxTotalUSD, RealmBudgets[r].MaxTotalUSD) against +// Redis at request time. // -// The verifier: -// - Rejects at signature time if MaxPerInvocationUSD > 0 and the -// invocation's max_cost_usd exceeds it (pure field comparison; -// no Redis). -// - In phase 6's hot path, the plugin tracks cumulative spend in -// Redis key `cost:ua:` and rejects when the total -// would meet or exceed MaxTotalUSD. The pure verifier (this -// package) is I/O-free and surfaces the Budget on Claims for the -// adapter to enforce. -type UserBudget struct { - MaxTotalUSD float64 `json:"max_total_usd"` - MaxPerInvocationUSD float64 `json:"max_per_invocation_usd"` +// Pointer + omitempty so layers that don't carry a budget produce +// byte-identical wire bytes to a pre-phase-11 macaroon when present +// only on the UA — important for the "single-swarm operators don't +// pay for any of this" promise. +type Budget struct { + MaxTotalUSD float64 `json:"max_total_usd,omitempty"` + MaxPerInvocationUSD float64 `json:"max_per_invocation_usd,omitempty"` + RealmBudgets map[string]RealmBudget `json:"realm_budgets,omitempty"` } +// UserBudget is a deprecated alias preserved so external callers can +// continue to read the budget field by its phase-4 name during the +// phase-11 cutover. New code should use Budget directly. +// +// Deprecated: use Budget. Kept only for the cutover window. +type UserBudget = Budget + // UserAuthorization is the org-signed envelope. The verifier strips // OrgSig, JCS-canonicalizes the rest, and verifies that canonical- // JSON-bytes against the org policy. // -// Budget is optional (pointer + omitempty). Absent ⇒ no UA-level -// budget enforcement; the wire bytes are byte-identical to a -// pre-budget macaroon, so old fixtures continue to verify. +// Phase 11 changes: +// - Permissions wrapper is gone; agents lifted to top-level. +// - The singular realm grant disappears; what realms a UA is +// permitted on is encoded by Budget.RealmBudgets (opt-in, +// multi-swarm only). +// - Budget is the rename of UserBudget; same wire shape extended +// with realm_budgets. type UserAuthorization struct { - UserID string `json:"user_id"` - UserPubkey PubKey `json:"user_pubkey"` - Permissions UserPermissions `json:"permissions"` - Budget *UserBudget `json:"budget,omitempty"` - IAT string `json:"iat"` - Exp string `json:"exp"` - Nonce string `json:"nonce"` - OrgSig Sig `json:"org_sig"` + UserID string `json:"user_id"` + UserPubkey PubKey `json:"user_pubkey"` + Agents []string `json:"agents"` + Budget *Budget `json:"budget,omitempty"` + IAT string `json:"iat"` + Exp string `json:"exp"` + Nonce string `json:"nonce"` + OrgSig Sig `json:"org_sig"` } // Invocation is the user-signed envelope. Strip UserSig and // canonicalize the rest to get the Ed25519 signing input. +// +// Phase 11: Realm (singular) is gone; the symmetric Budget block is +// new and optional — a single-swarm deployment can leave it absent +// and rely on the UA-level caps alone. type Invocation struct { - Realm string `json:"realm"` Agents []string `json:"agents"` RunID string `json:"run_id"` MaxCostUSD float64 `json:"max_cost_usd"` MaxSteps int `json:"max_steps"` + Budget *Budget `json:"budget,omitempty"` IAT string `json:"iat"` Exp string `json:"exp"` Nonce string `json:"nonce"` @@ -132,11 +154,17 @@ type Invocation struct { // AttenuationCaveats is the narrowed-scope object a parent agent // crafts when spawning a sub-agent. The HMAC binds it to the parent's // signature. +// +// Phase 11: Budget is the new optional block carrying realm_budgets; +// parents on multi-swarm deployments use it to authorize sub-agents +// to spend on specific swarms with locally-attenuated caps (no Hive +// round-trip required to cross realms). type AttenuationCaveats struct { Agents []string `json:"agents"` MaxCostUSD float64 `json:"max_cost_usd"` MaxSteps int `json:"max_steps"` RunID string `json:"run_id"` + Budget *Budget `json:"budget,omitempty"` Exp string `json:"exp"` Nonce string `json:"nonce"` } @@ -160,10 +188,15 @@ type Macaroon struct { // EffectiveCaveats is the invocation's caveats narrowed by every // attenuation in the chain. This is what the plugin enforces. +// +// Budget carries the layer's narrowed Budget block (or nil if no +// budget appears anywhere in the chain). The plugin reads +// Budget.RealmBudgets for the per-realm membership + cap check. type EffectiveCaveats struct { Agents []string MaxCostUSD float64 MaxSteps int + Budget *Budget Exp string } @@ -177,15 +210,25 @@ type EffectiveCaveats struct { // budget — adapters MUST treat nil as "no UA-level cap" rather than // substituting defaults; absent budget is a design choice, not // missing data. +// +// Phase 11: +// - Realm (the singular invocation realm) is gone. +// - PermittedRealms is the set of realm-ids the verified chain +// authorizes spend on. Derived from EffectiveCaveats.Budget. +// RealmBudgets — sorted, deduplicated, or nil when no +// realm_budgets appears anywhere in the chain (single-swarm +// deployments). The plugin's realm-membership check uses +// EffectiveCaveats.Budget.RealmBudgets directly; PermittedRealms +// is for logging and observability. type Claims struct { OrgID string UserID string - Realm string AgentName string // last element of the final agents list RunID string // innermost attenuation's run_id, or invocation's if none EffectiveCaveats EffectiveCaveats - UANonce string // ua.nonce; key for cost:ua: - UABudget *UserBudget // nil if UA carried no budget - Nonces []string // [ua.nonce, inv.nonce, atts[*].caveats.nonce] - IAT string // invocation.iat + UANonce string // ua.nonce; key for cost:ua: + UABudget *Budget // nil if UA carried no budget + PermittedRealms []string // sorted keys of EffectiveCaveats.Budget.RealmBudgets, or nil + Nonces []string // [ua.nonce, inv.nonce, atts[*].caveats.nonce] + IAT string // invocation.iat } diff --git a/gateway/auth/go/verify.go b/gateway/auth/go/verify.go index 3c9a2dabd..d15991ec5 100644 --- a/gateway/auth/go/verify.go +++ b/gateway/auth/go/verify.go @@ -4,6 +4,7 @@ import ( "crypto/hmac" "encoding/json" "fmt" + "sort" "time" ) @@ -52,7 +53,7 @@ func VerifyJSON(raw []byte, policy Policy, now time.Time) (*Claims, error) { return nil, err } - effective, runID, attNonces, err := walkAttenuations(&m.Invocation, m.Attenuations, now) + effective, runID, attNonces, err := walkAttenuations(&m.Invocation, m.UserAuthorization.Budget, m.Attenuations, now) if err != nil { return nil, err } @@ -69,12 +70,12 @@ func VerifyJSON(raw []byte, policy Policy, now time.Time) (*Claims, error) { return &Claims{ OrgID: m.OrgID, UserID: m.UserAuthorization.UserID, - Realm: m.Invocation.Realm, AgentName: agentName, RunID: runID, EffectiveCaveats: effective, UANonce: m.UserAuthorization.Nonce, UABudget: m.UserAuthorization.Budget, + PermittedRealms: permittedRealms(&effective), Nonces: nonces, IAT: m.Invocation.IAT, }, nil @@ -201,13 +202,17 @@ func verifyInvocation(inv *Invocation, ua *UserAuthorization) error { return nil } +// enforceInvocationCaveats is the UA→invocation boundary check. This +// is the one boundary where `agents` narrows (`child ⊆ parent`) — the +// user grants a set of agents on the UA, and the invocation picks a +// subset to actually use this run. Subsequent attenuation boundaries +// extend lineage (`child ⊇ parent`); see narrowAttenuation. func enforceInvocationCaveats(inv *Invocation, ua *UserAuthorization, now time.Time) error { - if !containsString(ua.Permissions.Realms, inv.Realm) { - return newError(ErrInvocationViolated, - fmt.Sprintf("realm %s not in user permissions", inv.Realm)) + if len(inv.Agents) == 0 { + return newError(ErrInvocationViolated, "invocation.agents must be non-empty") } for _, a := range inv.Agents { - if !containsString(ua.Permissions.Agents, a) { + if !containsString(ua.Agents, a) { return newError(ErrInvocationViolated, fmt.Sprintf("agent %s not in user permissions", a)) } @@ -215,6 +220,12 @@ func enforceInvocationCaveats(inv *Invocation, ua *UserAuthorization, now time.T if expBefore(inv.Exp, now) { return newError(ErrMacaroonExpired, fmt.Sprintf("invocation expired at %s", inv.Exp)) } + // Exp narrowing: invocation must not outlive the UA. Signature- + // time field comparison, no clock dependency. + if inv.Exp > ua.Exp { + return newError(ErrInvocationViolated, + fmt.Sprintf("invocation exp %s > ua exp %s", inv.Exp, ua.Exp)) + } // Per-invocation budget cap. Pure signature-time check — the // cumulative cap (MaxTotalUSD) is enforced by the adapter via // Redis in PreLLMHook, not here. @@ -224,21 +235,34 @@ func enforceInvocationCaveats(inv *Invocation, ua *UserAuthorization, now time.T fmt.Sprintf("invocation max_cost_usd %v > ua.budget.max_per_invocation_usd %v", inv.MaxCostUSD, ua.Budget.MaxPerInvocationUSD)) } + // Budget narrowing: phase 11's symmetric rule applies between + // the UA's Budget and the invocation's Budget block. The + // invocation block, when present, must not widen any axis the UA + // constrained. + if err := narrowBudget(ua.Budget, inv.Budget); err != nil { + return err + } return nil } // ─── attenuation chain walk ─────────────────────────────────────────── -func walkAttenuations(inv *Invocation, atts []Attenuation, now time.Time) (EffectiveCaveats, string, []string, error) { +func walkAttenuations(inv *Invocation, uaBudget *Budget, atts []Attenuation, now time.Time) (EffectiveCaveats, string, []string, error) { prevSigBytes, err := HexToBytes(inv.UserSig.Sig) if err != nil { return EffectiveCaveats{}, "", nil, newError(ErrAttenuationInvalid, fmt.Sprintf("invocation user_sig hex: %v", err)) } + // Effective Budget at the invocation layer = the invocation's + // own block when set, else inherit the UA's. This mirrors the + // "Mixed mode" rule in phase-11: parent has budget, child omits + // → child inherits unchanged. Without this, realm_budgets set + // only at the UA wouldn't propagate to the membership check. effective := EffectiveCaveats{ Agents: append([]string{}, inv.Agents...), MaxCostUSD: inv.MaxCostUSD, MaxSteps: inv.MaxSteps, + Budget: mergeAttenuationBudget(uaBudget, inv.Budget), Exp: inv.Exp, } runID := inv.RunID @@ -259,7 +283,7 @@ func walkAttenuations(inv *Invocation, atts []Attenuation, now time.Time) (Effec return effective, runID, nonces, newError(ErrAttenuationInvalid, fmt.Sprintf("attenuation[%d] hmac mismatch", i)) } - if err := enforceNarrowing(effective, att.Caveats); err != nil { + if err := narrowAttenuation(effective, att.Caveats); err != nil { return effective, runID, nonces, err } if expBefore(att.Caveats.Exp, now) { @@ -270,6 +294,7 @@ func walkAttenuations(inv *Invocation, atts []Attenuation, now time.Time) (Effec Agents: append([]string{}, att.Caveats.Agents...), MaxCostUSD: att.Caveats.MaxCostUSD, MaxSteps: att.Caveats.MaxSteps, + Budget: mergeAttenuationBudget(effective.Budget, att.Caveats.Budget), Exp: att.Caveats.Exp, } runID = att.Caveats.RunID @@ -280,9 +305,15 @@ func walkAttenuations(inv *Invocation, atts []Attenuation, now time.Time) (Effec return effective, runID, nonces, nil } -func enforceNarrowing(parent EffectiveCaveats, child AttenuationCaveats) error { +// narrowAttenuation is the parent→child check at every attenuation +// boundary. Agents is the lineage-extension axis (child ⊇ parent); +// every other axis is shrink-only (child ≤ parent). This is the half +// of phase 11's symmetric rule that differs from the UA→invocation +// boundary handled in enforceInvocationCaveats. +func narrowAttenuation(parent EffectiveCaveats, child AttenuationCaveats) error { // agents: child ⊇ parent — child must include every parent entry, // and may add. (The agents list grows; cost/steps/exp shrink.) + // The last entry remains "the most-specific agent" for billing. for _, a := range parent.Agents { if !containsString(child.Agents, a) { return newError(ErrAttenuationWidened, @@ -303,9 +334,95 @@ func enforceNarrowing(parent EffectiveCaveats, child AttenuationCaveats) error { return newError(ErrAttenuationWidened, fmt.Sprintf("child exp %s > parent %s", child.Exp, parent.Exp)) } + if err := narrowBudget(parent.Budget, child.Budget); err != nil { + return err + } + return nil +} + +// narrowBudget enforces the symmetric budget-narrowing rule between +// any parent→child layer boundary (UA→invocation, invocation→ +// attenuation, attenuation→attenuation). Returns ErrAttenuationWidened +// when the child widens any axis. nil child means "inherits parent +// unchanged"; nil parent + non-nil child means "child introduces a +// constraint that didn't exist" — that's narrowing, allowed. +// +// Realm-budgets narrowing (rule 4 in phase-11): for each realm-id +// key in child.RealmBudgets, the same key must exist in +// parent.RealmBudgets (if parent set realm_budgets at all), and the +// child's per-realm cap must be ≤ parent's. Child may also OMIT +// realms the parent permitted — that's narrowing, allowed. +func narrowBudget(parent, child *Budget) error { + if child == nil { + return nil + } + if parent != nil { + if parent.MaxPerInvocationUSD > 0 && child.MaxPerInvocationUSD > 0 && + child.MaxPerInvocationUSD > parent.MaxPerInvocationUSD { + return newError(ErrAttenuationWidened, + fmt.Sprintf("child budget.max_per_invocation_usd %v > parent %v", + child.MaxPerInvocationUSD, parent.MaxPerInvocationUSD)) + } + if parent.MaxTotalUSD > 0 && child.MaxTotalUSD > 0 && + child.MaxTotalUSD > parent.MaxTotalUSD { + return newError(ErrAttenuationWidened, + fmt.Sprintf("child budget.max_total_usd %v > parent %v", + child.MaxTotalUSD, parent.MaxTotalUSD)) + } + } + if len(child.RealmBudgets) == 0 { + return nil + } + // If parent set realm_budgets, every child key must appear in it + // and not widen its cap. If parent did NOT set realm_budgets, + // the child is introducing per-realm scoping that didn't exist + // upstream — that's narrowing, allowed. + if parent == nil || len(parent.RealmBudgets) == 0 { + return nil + } + for r, cb := range child.RealmBudgets { + pb, ok := parent.RealmBudgets[r] + if !ok { + return newError(ErrAttenuationWidened, + fmt.Sprintf("child budget.realm_budgets[%s] not in parent", r)) + } + if pb.MaxTotalUSD > 0 && cb.MaxTotalUSD > 0 && + cb.MaxTotalUSD > pb.MaxTotalUSD { + return newError(ErrAttenuationWidened, + fmt.Sprintf("child budget.realm_budgets[%s].max_total_usd %v > parent %v", + r, cb.MaxTotalUSD, pb.MaxTotalUSD)) + } + } return nil } +// mergeAttenuationBudget propagates the effective Budget down the +// chain. The rule mirrors narrowBudget's intent: a child that omits +// budget inherits the parent's; a child that sets budget replaces +// the parent's (already validated as narrowing by narrowBudget). +func mergeAttenuationBudget(parent, child *Budget) *Budget { + if child == nil { + return parent + } + return child +} + +// permittedRealms returns the sorted list of realm-ids the effective +// caveats authorize spend on. Returns nil when no realm_budgets +// appears anywhere in the chain — single-swarm deployments rely on +// this nil-ness to skip the membership check entirely. +func permittedRealms(eff *EffectiveCaveats) []string { + if eff == nil || eff.Budget == nil || len(eff.Budget.RealmBudgets) == 0 { + return nil + } + out := make([]string, 0, len(eff.Budget.RealmBudgets)) + for r := range eff.Budget.RealmBudgets { + out = append(out, r) + } + sort.Strings(out) + return out +} + // ─── helpers ────────────────────────────────────────────────────────── func expBefore(iso string, now time.Time) bool { diff --git a/gateway/auth/ts/package.json b/gateway/auth/ts/package.json index 9a8637c03..c42ae4d34 100644 --- a/gateway/auth/ts/package.json +++ b/gateway/auth/ts/package.json @@ -1,6 +1,6 @@ { "name": "gatekey", - "version": "0.1.0", + "version": "0.1.1", "description": "Three-layer macaroon signer, attenuator, and verifier for cryptographic LLM governance", "type": "module", "main": "./dist/index.js", diff --git a/gateway/auth/ts/scripts/regenerate-fixtures.ts b/gateway/auth/ts/scripts/regenerate-fixtures.ts index 3a9877d1d..714f4b9b9 100644 --- a/gateway/auth/ts/scripts/regenerate-fixtures.ts +++ b/gateway/auth/ts/scripts/regenerate-fixtures.ts @@ -34,6 +34,7 @@ import { import type { Attenuation, AttenuationCaveats, + Budget, Invocation, InvocationUnsigned, Macaroon, @@ -87,10 +88,10 @@ function baseUserAuthorization(): UserAuthorizationUnsigned { return { user_id: "u_alice", user_pubkey: { alg: "ed25519", key: userPubHex }, - permissions: { - realms: ["w1", "w2"], - agents: ["coder", "browser", "web-search", "repair-agent"], - }, + // Phase 11: agents lifted out of the (deleted) `permissions` + // wrapper. No singular `realm` grant — multi-swarm scoping is + // encoded in `budget.realm_budgets` when needed. + agents: ["coder", "browser", "web-search", "repair-agent"], iat: UA_IAT, exp: UA_EXP, nonce: "9f4e1c8b2a3d4e5f6a7b8c9d0e1f2a3b", @@ -99,7 +100,8 @@ function baseUserAuthorization(): UserAuthorizationUnsigned { function baseInvocation(): InvocationUnsigned { return { - realm: "w1", + // Phase 11: no `realm` field. Single-swarm deployments need no + // realm scoping; multi-swarm scoping rides on `budget.realm_budgets`. agents: ["coder"], run_id: "r_01h8alpharootinvocation00", max_cost_usd: 5.0, @@ -128,17 +130,18 @@ interface FixtureExpected { claims: { org_id: string; user_id: string; - realm: string; agent_name: string; run_id: string; effective_caveats: { agents: string[]; max_cost_usd: number; max_steps: number; + budget: Budget | null; exp: string; }; ua_nonce: string; - ua_budget: { max_total_usd: number; max_per_invocation_usd: number } | null; + ua_budget: Budget | null; + permitted_realms: string[] | null; nonces: string[]; iat: string; }; @@ -186,26 +189,38 @@ function computeExpected( prevSig = hexToBytes(att.hmac); } - // Build claims by replaying narrowing locally (mirror of the verifier). - let effective = { + // Build claims by replaying narrowing locally (mirror of the + // verifier). Phase 11 effective_caveats carries the propagated + // Budget block; permitted_realms is the sorted keys of the + // effective budget's realm_budgets, or null. Inherit the UA's + // budget when the invocation omits its own ("Mixed mode" rule). + let effectiveBudget: Budget | null = + signedInv.budget ?? signedUa.budget ?? null; + let effective: FixtureExpected["claims"]["effective_caveats"] = { agents: [...signedInv.agents], max_cost_usd: signedInv.max_cost_usd, max_steps: signedInv.max_steps, + budget: effectiveBudget, exp: signedInv.exp, }; let runId = signedInv.run_id; const attNonces: string[] = []; for (const att of atts) { + effectiveBudget = att.caveats.budget ?? effectiveBudget; effective = { agents: att.caveats.agents, max_cost_usd: att.caveats.max_cost_usd, max_steps: att.caveats.max_steps, + budget: effectiveBudget, exp: att.caveats.exp, }; runId = att.caveats.run_id; attNonces.push(att.caveats.nonce); } const agentName = effective.agents[effective.agents.length - 1] ?? ""; + const permittedRealms = effectiveBudget?.realm_budgets + ? Object.keys(effectiveBudget.realm_budgets).sort() + : null; const macaroonCanonical = jcs(macaroon as unknown as Record); const macaroonB64 = bytesToBase64url(utf8Bytes(macaroonCanonical)); @@ -221,12 +236,12 @@ function computeExpected( claims: { org_id: macaroon.org_id, user_id: signedUa.user_id, - realm: signedInv.realm, agent_name: agentName, run_id: runId, effective_caveats: effective, ua_nonce: signedUa.nonce, ua_budget: signedUa.budget ?? null, + permitted_realms: permittedRealms, nonces: [signedUa.nonce, signedInv.nonce, ...attNonces], iat: signedInv.iat, }, @@ -253,7 +268,7 @@ function makeFixture01Simple() { const macaroon = buildMacaroon("org_acme", signedUa, signedInv, []); return { description: - "single-key org, custodial phase 1: one user_authorization, one invocation, zero attenuations", + "single-key org, custodial phase 1: one user_authorization, one invocation, zero attenuations. Single-swarm shape — no realm fields anywhere.", inputs: { org_id: "org_acme", org_priv_hex: ORG_PRIV_HEX, @@ -289,7 +304,7 @@ function makeFixture02OneAttenuation() { const macaroon = buildMacaroon("org_acme", signedUa, signedInv, [att1]); return { description: - "single-key org with one sub-agent attenuation narrowing budget and adding a web-search agent", + "single-key org with one sub-agent attenuation narrowing budget and adding a web-search agent (lineage extension)", inputs: { org_id: "org_acme", org_priv_hex: ORG_PRIV_HEX, @@ -457,6 +472,141 @@ function makeFixture04Multisig() { }; } +function makeFixture06MultiRealm() { + // Phase 11 multi-swarm scenario: the org signs a UA with per-realm + // caps for two swarms (w1, w2). The user's invocation narrows the + // realm_budgets to a subset with smaller caps for this run. + // + // The verifier checks budget narrowing at the UA→invocation + // boundary; the plugin (out of scope for the pure verifier) reads + // claims.permitted_realms to assert this swarm's realm_id is in + // the set, then enforces the per-realm cap against Redis. + const ua: UserAuthorizationUnsigned = { + ...baseUserAuthorization(), + budget: { + max_total_usd: 1000.0, + max_per_invocation_usd: 25.0, + realm_budgets: { + w1: { max_total_usd: 500.0 }, + w2: { max_total_usd: 200.0 }, + }, + }, + nonce: "f06e1d2c3b4a5968778899aabbccddee", + }; + const inv: InvocationUnsigned = { + ...baseInvocation(), + run_id: "r_01h8multirealmrun000000000", + max_cost_usd: 5.0, + nonce: "f16e1d2c3b4a5968778899aabbccddee", + budget: { + // Narrowing per axis: child caps ≤ parent caps; both child + // realms exist in parent's set. + realm_budgets: { + w1: { max_total_usd: 5.0 }, + w2: { max_total_usd: 2.0 }, + }, + }, + }; + const signedUa = signUserAuthorizationSingle(ua, orgPriv); + const signedInv = signInvocation(inv, userPriv); + const macaroon = buildMacaroon("org_acme", signedUa, signedInv, []); + return { + description: + "multi-realm UA + invocation: org grants per-realm caps {w1:$500, w2:$200}, invocation narrows to {w1:$5, w2:$2}; no attenuations", + inputs: { + org_id: "org_acme", + org_priv_hex: ORG_PRIV_HEX, + user_priv_hex: USER_PRIV_HEX, + policy: { + type: "single" as const, + key: { alg: "ecdsa-secp256k1-sha256" as const, key: orgPubHex }, + }, + ua_unsigned: ua, + inv_unsigned: inv, + atts_unsigned: [] as AttenuationCaveats[], + }, + expected: computeExpected(ua, signedUa, inv, signedInv, [], macaroon), + }; +} + +function makeFixture07CrossRealmAttenuation() { + // Cross-realm sub-agent spawn: the parent invocation authorizes + // spend on w1+w2, then attenuates locally to delegate a + // sub-agent that should only spend on w2 with a smaller cap. No + // Hive round-trip on the spawn path; the verifier checks the + // HMAC chain + symmetric budget narrowing. + const ua: UserAuthorizationUnsigned = { + ...baseUserAuthorization(), + budget: { + max_total_usd: 1000.0, + max_per_invocation_usd: 25.0, + realm_budgets: { + w1: { max_total_usd: 500.0 }, + w2: { max_total_usd: 200.0 }, + }, + }, + nonce: "07e1d2c3b4a596778899aabbccddee01", + }; + const inv: InvocationUnsigned = { + ...baseInvocation(), + run_id: "r_01h8crossrealmparent00000", + max_cost_usd: 5.0, + nonce: "07e1d2c3b4a596778899aabbccddee02", + budget: { + realm_budgets: { + w1: { max_total_usd: 5.0 }, + w2: { max_total_usd: 4.0 }, + }, + }, + }; + const signedUa = signUserAuthorizationSingle(ua, orgPriv); + const signedInv = signInvocation(inv, userPriv); + + const subAgentCaveats: AttenuationCaveats = { + // Lineage extension: child agents ⊇ parent agents. + agents: ["coder", "web-search"], + max_cost_usd: 2.0, + max_steps: 40, + run_id: "r_01h8crossrealmsubchild00", + exp: ATT1_EXP, + nonce: "07e1d2c3b4a596778899aabbccddee03", + budget: { + // Narrow to a single realm with a smaller cap. Parent has w1+w2, + // child drops w1 (allowed) and tightens w2's cap. + realm_budgets: { + w2: { max_total_usd: 1.0 }, + }, + }, + }; + const subAgent = attenuate(invocationSigBytes(signedInv), subAgentCaveats); + + const macaroon = buildMacaroon("org_acme", signedUa, signedInv, [subAgent]); + return { + description: + "cross-realm sub-agent attenuation: parent invocation allows w1+w2, child attenuates to w2 only with a smaller cap (HMAC-chained, no issuer round-trip)", + inputs: { + org_id: "org_acme", + org_priv_hex: ORG_PRIV_HEX, + user_priv_hex: USER_PRIV_HEX, + policy: { + type: "single" as const, + key: { alg: "ecdsa-secp256k1-sha256" as const, key: orgPubHex }, + }, + ua_unsigned: ua, + inv_unsigned: inv, + atts_unsigned: [subAgentCaveats], + }, + expected: computeExpected( + ua, + signedUa, + inv, + signedInv, + [subAgent], + macaroon, + ), + }; +} + // ─── keys.json (the deterministic seed set) ─────────────────────────── function makeKeysJson() { @@ -509,6 +659,8 @@ function main() { writeJson(join(FIXTURES_DIR, "03-two-attenuations.json"), makeFixture03TwoAttenuations()); writeJson(join(FIXTURES_DIR, "04-multisig-2of3.json"), makeFixture04Multisig()); writeJson(join(FIXTURES_DIR, "05-budget-envelope.json"), makeFixture05BudgetEnvelope()); + writeJson(join(FIXTURES_DIR, "06-multi-realm.json"), makeFixture06MultiRealm()); + writeJson(join(FIXTURES_DIR, "07-cross-realm-attenuation.json"), makeFixture07CrossRealmAttenuation()); } main(); diff --git a/gateway/auth/ts/src/index.ts b/gateway/auth/ts/src/index.ts index bd099b8d1..86b616d39 100644 --- a/gateway/auth/ts/src/index.ts +++ b/gateway/auth/ts/src/index.ts @@ -19,7 +19,8 @@ export type { MultisigPolicy, Policy, // macaroon layers - UserPermissions, + RealmBudget, + Budget, UserBudget, UserAuthorizationUnsigned, UserAuthorization, diff --git a/gateway/auth/ts/src/types.ts b/gateway/auth/ts/src/types.ts index a51a948e0..36440b27d 100644 --- a/gateway/auth/ts/src/types.ts +++ b/gateway/auth/ts/src/types.ts @@ -1,6 +1,8 @@ /** * Wire types for the three-layer macaroon. Names and shapes match - * `gateway/plans/phases/phase-4-macaroon-shape.md` exactly. + * `gateway/plans/phases/phase-4-macaroon-shape.md` and the + * symmetric-recursive refinement in + * `gateway/plans/phases/phase-11-symmetric-recursive-authorization.md`. * * All binary fields (pubkeys, signatures, HMACs, nonces) are lowercase * hex strings WITHOUT `0x` prefix. All timestamps are RFC 3339 / ISO @@ -69,48 +71,66 @@ export type Policy = SinglePolicy | MultisigPolicy; // ─── macaroon layers ────────────────────────────────────────────────── -export interface UserPermissions { - realms: string[]; - agents: string[]; +/** + * Per-realm spending cap inside a `Budget`. Phase 11 adds the + * `realm_budgets` map so multi-swarm deployments can pin exact + * per-swarm cumulative caps without relying on the implicit + * "max_total_usd leaks across swarms" behavior. + * + * Only `max_total_usd` is defined today. Per-realm step / rate caps + * are an open question in phase 11 (see "Open questions" §2) and + * can be added without breaking compatibility — `RealmBudget` is a + * struct, not a bare number, precisely to leave that door open. + */ +export interface RealmBudget { + max_total_usd: number; } /** - * Org-signed spending envelope for a `user_authorization`. Optional; - * if omitted, no UA-level budget enforcement happens. + * Spending envelope carried by any signed layer. Phase 11 renamed + * `UserBudget` → `Budget` so the same shape appears on + * `UserAuthorization`, `Invocation`, and `AttenuationCaveats`. + * Children narrow against parents using the rules in `verify.ts`'s + * narrowing functions. * - * Both fields are independently optional. Zero means "no cap on this - * axis" — consistent with the empty-by-default convention used for - * agent_budgets in phase 6. + * All fields are independently optional. Zero / undefined means "no + * cap on this axis" — consistent with the empty-by-default + * convention used for `agent_budgets` in phase 6. The verifier + * checks structural narrowing (child ≤ parent at each axis); the + * adapter enforces the cumulative caps (`max_total_usd`, + * `realm_budgets[r].max_total_usd`) against Redis at request time. * - * See `gateway/plans/phases/phase-4-macaroon-shape.md` ("Budget - * envelope") for the motivating cold-storage flow: org leader signs - * a UA from cold storage with `max_total_usd: $X`, the employee's - * hot key signs many invocations under it through the week. - * - * The verifier: - * - Rejects at signature time if `max_per_invocation_usd > 0` and - * the invocation's `max_cost_usd` exceeds it (pure field - * comparison; no Redis). - * - In phase 6's hot path, the plugin tracks cumulative spend in - * Redis key `cost:ua:` and rejects when the total - * would meet or exceed `max_total_usd`. The pure verifier (this - * package) is I/O-free and surfaces the budget on Claims for - * the adapter to enforce. + * Layers that omit `budget` produce byte-identical wire bytes to a + * pre-phase-11 macaroon when the field was present only on the UA — + * important for the "single-swarm operators don't pay for any of + * this" promise. */ -export interface UserBudget { - max_total_usd: number; - max_per_invocation_usd: number; +export interface Budget { + max_total_usd?: number; + max_per_invocation_usd?: number; + realm_budgets?: Record; } +/** + * Deprecated alias preserved so external callers can continue to + * read the budget field by its phase-4 name during the phase-11 + * cutover. New code should use `Budget` directly. + */ +export type UserBudget = Budget; + export interface UserAuthorizationUnsigned { user_id: string; user_pubkey: Ed25519PubKey; - permissions: UserPermissions; + /** + * Permitted agents (phase 11 lifted this from the `permissions` + * wrapper to top-level). Invocations narrow by picking a subset. + */ + agents: string[]; /** * Optional. Absent on the wire ⇒ no UA-level budget enforcement. * Byte-identical to a pre-budget macaroon when omitted. */ - budget?: UserBudget; + budget?: Budget; iat: string; exp: string; nonce: string; @@ -121,11 +141,15 @@ export interface UserAuthorization extends UserAuthorizationUnsigned { } export interface InvocationUnsigned { - realm: string; agents: string[]; run_id: string; max_cost_usd: number; max_steps: number; + /** + * Optional invocation-level budget block. When present, must + * narrow against the UA's budget (phase 11 symmetric rule). + */ + budget?: Budget; iat: string; exp: string; nonce: string; @@ -140,6 +164,12 @@ export interface AttenuationCaveats { max_cost_usd: number; max_steps: number; run_id: string; + /** + * Optional attenuation-level budget block. Used by parents + * spawning cross-realm sub-agents to authorize spend on specific + * swarms with locally-attenuated caps (no Hive round-trip). + */ + budget?: Budget; exp: string; nonce: string; } @@ -163,18 +193,22 @@ export interface Macaroon { /** * Effective caveats: invocation caveats narrowed by every attenuation in * the chain. What the plugin actually enforces. + * + * `budget` carries the layer's narrowed Budget block (or null if no + * budget appears anywhere in the chain). The plugin reads + * `budget.realm_budgets` for the per-realm membership + cap check. */ export interface EffectiveCaveats { agents: string[]; max_cost_usd: number; max_steps: number; + budget: Budget | null; exp: string; } export interface Claims { org_id: string; user_id: string; - realm: string; /** Most-specific agent name (last element of the final `agents` list). */ agent_name: string; /** Run id of the innermost attenuation, or the invocation if no attenuations. */ @@ -193,7 +227,16 @@ export interface Claims { * UA-level cap" rather than substituting defaults; absent budget * is a design choice, not missing data. */ - ua_budget: UserBudget | null; + ua_budget: Budget | null; + /** + * Sorted list of realm-ids the verified chain authorizes spend + * on. Derived from `effective_caveats.budget.realm_budgets` — + * sorted keys, or `null` when no `realm_budgets` appears anywhere + * in the chain (single-swarm deployments). The plugin's + * realm-membership check uses `effective_caveats.budget.realm_budgets` + * directly; `permitted_realms` is for logging and observability. + */ + permitted_realms: string[] | null; /** Nonces in order: user_authorization, invocation, attenuations[0..]. */ nonces: string[]; /** Invocation iat — used by adapters for revoke_user_before checks. */ diff --git a/gateway/auth/ts/src/verify.ts b/gateway/auth/ts/src/verify.ts index 803a74244..d70fc8393 100644 --- a/gateway/auth/ts/src/verify.ts +++ b/gateway/auth/ts/src/verify.ts @@ -15,6 +15,7 @@ import { ecdsaVerify, ed25519Verify } from "./sigs.js"; import type { Attenuation, AttenuationCaveats, + Budget, Claims, EffectiveCaveats, Invocation, @@ -79,6 +80,7 @@ export function verify( const { effective, runId, agentName, nonces } = walkAttenuations( m.invocation, + m.user_authorization.budget ?? null, m.attenuations, now, ); @@ -86,12 +88,12 @@ export function verify( return { org_id: m.org_id, user_id: m.user_authorization.user_id, - realm: m.invocation.realm, agent_name: agentName, run_id: runId, effective_caveats: effective, ua_nonce: m.user_authorization.nonce, ua_budget: m.user_authorization.budget ?? null, + permitted_realms: permittedRealms(effective), nonces: [m.user_authorization.nonce, m.invocation.nonce, ...nonces], iat: m.invocation.iat, }; @@ -193,28 +195,42 @@ function verifyInvocation(inv: Invocation, ua: UserAuthorization): void { } } +/** + * UA→invocation boundary check. This is the one boundary where + * `agents` narrows (`child ⊆ parent`) — the user grants a set of + * agents on the UA, and the invocation picks a subset to actually + * use this run. Subsequent attenuation boundaries extend lineage + * (`child ⊇ parent`); see `narrowAttenuation`. + */ function enforceInvocationCaveats( inv: Invocation, ua: UserAuthorization, now: Date, ): void { - if (!ua.permissions.realms.includes(inv.realm)) { + if (inv.agents.length === 0) { throw new VerifyError("invocation_violated", - `realm ${inv.realm} not in user permissions`); + "invocation.agents must be non-empty"); } for (const a of inv.agents) { - if (!ua.permissions.agents.includes(a)) { + if (!ua.agents.includes(a)) { throw new VerifyError("invocation_violated", `agent ${a} not in user permissions`); } } if (asDate(inv.exp) < now) { throw new VerifyError("macaroon_expired", `invocation expired at ${inv.exp}`); } + // Exp narrowing: invocation must not outlive the UA. Signature- + // time field comparison, no clock dependency. + if (inv.exp > ua.exp) { + throw new VerifyError("invocation_violated", + `invocation exp ${inv.exp} > ua exp ${ua.exp}`); + } // Per-invocation budget cap. Pure signature-time check — the // cumulative cap (max_total_usd) is enforced by the adapter via // Redis in PreLLMHook, not here. if ( ua.budget && + ua.budget.max_per_invocation_usd !== undefined && ua.budget.max_per_invocation_usd > 0 && inv.max_cost_usd > ua.budget.max_per_invocation_usd ) { @@ -223,6 +239,10 @@ function enforceInvocationCaveats( `invocation max_cost_usd ${inv.max_cost_usd} > ua.budget.max_per_invocation_usd ${ua.budget.max_per_invocation_usd}`, ); } + // Budget narrowing: phase 11's symmetric rule applies between the + // UA's Budget and the invocation's Budget block. The invocation + // block, when present, must not widen any axis the UA constrained. + narrowBudget(ua.budget ?? null, inv.budget ?? null); } // ─── attenuation chain walk ─────────────────────────────────────────── @@ -236,14 +256,21 @@ interface WalkResult { function walkAttenuations( inv: Invocation, + uaBudget: Budget | null, atts: Attenuation[], now: Date, ): WalkResult { let prevSigBytes = hexToBytes(inv.user_sig.sig); + // Effective Budget at the invocation layer = the invocation's own + // block when set, else inherit the UA's. This mirrors the "Mixed + // mode" rule in phase-11: parent has budget, child omits → child + // inherits unchanged. Without this, realm_budgets set only at the + // UA wouldn't propagate to the membership check. let effective: EffectiveCaveats = { agents: [...inv.agents], max_cost_usd: inv.max_cost_usd, max_steps: inv.max_steps, + budget: mergeAttenuationBudget(uaBudget, inv.budget ?? null), exp: inv.exp, }; let runId = inv.run_id; @@ -255,7 +282,7 @@ function walkAttenuations( if (expectedHex !== att.hmac) { throw new VerifyError("attenuation_invalid", "hmac mismatch"); } - enforceNarrowing(effective, att.caveats); + narrowAttenuation(effective, att.caveats); if (asDate(att.caveats.exp) < now) { throw new VerifyError("macaroon_expired", `attenuation expired at ${att.caveats.exp}`); } @@ -263,6 +290,7 @@ function walkAttenuations( agents: att.caveats.agents, max_cost_usd: att.caveats.max_cost_usd, max_steps: att.caveats.max_steps, + budget: mergeAttenuationBudget(effective.budget, att.caveats.budget ?? null), exp: att.caveats.exp, }; runId = att.caveats.run_id; @@ -274,8 +302,16 @@ function walkAttenuations( return { effective, runId, agentName, nonces }; } -function enforceNarrowing(parent: EffectiveCaveats, child: AttenuationCaveats): void { - // agents: child must include every parent entry (child ⊇ parent) +/** + * Parent→child check at every attenuation boundary. Agents is the + * lineage-extension axis (child ⊇ parent); every other axis is + * shrink-only (child ≤ parent). This is the half of phase 11's + * symmetric rule that differs from the UA→invocation boundary + * handled in `enforceInvocationCaveats`. + */ +function narrowAttenuation(parent: EffectiveCaveats, child: AttenuationCaveats): void { + // agents: child must include every parent entry (child ⊇ parent). + // The last entry remains "the most-specific agent" for billing. for (const a of parent.agents) { if (!child.agents.includes(a)) { throw new VerifyError("attenuation_widened", @@ -295,6 +331,83 @@ function enforceNarrowing(parent: EffectiveCaveats, child: AttenuationCaveats): throw new VerifyError("attenuation_widened", `child exp ${child.exp} > parent ${parent.exp}`); } + narrowBudget(parent.budget, child.budget ?? null); +} + +/** + * Symmetric budget-narrowing rule between any parent→child layer + * boundary (UA→invocation, invocation→attenuation, attenuation→ + * attenuation). Throws `attenuation_widened` when the child widens + * any axis. null child means "inherits parent unchanged"; null + * parent + non-null child means "child introduces a constraint that + * didn't exist" — that's narrowing, allowed. + * + * Realm-budgets narrowing (rule 4 in phase-11): for each realm-id + * key in `child.realm_budgets`, the same key must exist in + * `parent.realm_budgets` (if parent set realm_budgets at all), and + * the child's per-realm cap must be ≤ parent's. Child may also OMIT + * realms the parent permitted — that's narrowing, allowed. + */ +function narrowBudget(parent: Budget | null, child: Budget | null): void { + if (!child) return; + if (parent) { + const ppi = parent.max_per_invocation_usd ?? 0; + const cpi = child.max_per_invocation_usd ?? 0; + if (ppi > 0 && cpi > 0 && cpi > ppi) { + throw new VerifyError("attenuation_widened", + `child budget.max_per_invocation_usd ${cpi} > parent ${ppi}`); + } + const pt = parent.max_total_usd ?? 0; + const ct = child.max_total_usd ?? 0; + if (pt > 0 && ct > 0 && ct > pt) { + throw new VerifyError("attenuation_widened", + `child budget.max_total_usd ${ct} > parent ${pt}`); + } + } + const childRealms = child.realm_budgets; + if (!childRealms || Object.keys(childRealms).length === 0) return; + // If parent set realm_budgets, every child key must appear in it + // and not widen its cap. If parent did NOT set realm_budgets, the + // child is introducing per-realm scoping that didn't exist upstream + // — that's narrowing, allowed. + const parentRealms = parent?.realm_budgets; + if (!parentRealms || Object.keys(parentRealms).length === 0) return; + for (const [r, cb] of Object.entries(childRealms)) { + const pb = parentRealms[r]; + if (!pb) { + throw new VerifyError("attenuation_widened", + `child budget.realm_budgets[${r}] not in parent`); + } + if (pb.max_total_usd > 0 && cb.max_total_usd > 0 && + cb.max_total_usd > pb.max_total_usd) { + throw new VerifyError("attenuation_widened", + `child budget.realm_budgets[${r}].max_total_usd ${cb.max_total_usd} > parent ${pb.max_total_usd}`); + } + } +} + +/** + * Propagate the effective Budget down the chain. The rule mirrors + * `narrowBudget`'s intent: a child that omits budget inherits the + * parent's; a child that sets budget replaces the parent's (already + * validated as narrowing by `narrowBudget`). + */ +function mergeAttenuationBudget(parent: Budget | null, child: Budget | null): Budget | null { + if (!child) return parent; + return child; +} + +/** + * Sorted list of realm-ids the effective caveats authorize spend on. + * Returns null when no `realm_budgets` appears anywhere in the chain + * — single-swarm deployments rely on this null-ness to skip the + * membership check entirely. + */ +function permittedRealms(eff: EffectiveCaveats): string[] | null { + if (!eff.budget || !eff.budget.realm_budgets) return null; + const keys = Object.keys(eff.budget.realm_budgets); + if (keys.length === 0) return null; + return keys.sort(); } // ─── helpers ────────────────────────────────────────────────────────── diff --git a/gateway/auth/ts/test/fixtures.test.ts b/gateway/auth/ts/test/fixtures.test.ts index ed371ce68..64407ecae 100644 --- a/gateway/auth/ts/test/fixtures.test.ts +++ b/gateway/auth/ts/test/fixtures.test.ts @@ -33,6 +33,7 @@ import { import type { Attenuation, AttenuationCaveats, + Budget, InvocationUnsigned, Macaroon, Policy, @@ -71,17 +72,18 @@ interface FixtureShape { claims: { org_id: string; user_id: string; - realm: string; agent_name: string; run_id: string; effective_caveats: { agents: string[]; max_cost_usd: number; max_steps: number; + budget: Budget | null; exp: string; }; ua_nonce: string; - ua_budget: { max_total_usd: number; max_per_invocation_usd: number } | null; + ua_budget: Budget | null; + permitted_realms: string[] | null; nonces: string[]; iat: string; }; @@ -195,12 +197,12 @@ for (const file of fixtureFiles()) { ); assert.equal(claims.org_id, fx.expected.claims.org_id); assert.equal(claims.user_id, fx.expected.claims.user_id); - assert.equal(claims.realm, fx.expected.claims.realm); assert.equal(claims.agent_name, fx.expected.claims.agent_name); assert.equal(claims.run_id, fx.expected.claims.run_id); assert.deepEqual(claims.effective_caveats, fx.expected.claims.effective_caveats); assert.equal(claims.ua_nonce, fx.expected.claims.ua_nonce); assert.deepEqual(claims.ua_budget, fx.expected.claims.ua_budget); + assert.deepEqual(claims.permitted_realms, fx.expected.claims.permitted_realms); assert.deepEqual(claims.nonces, fx.expected.claims.nonces); assert.equal(claims.iat, fx.expected.claims.iat); }); diff --git a/gateway/internal/adminapi/observability.go b/gateway/internal/adminapi/observability.go index b52b8385e..8ab42ef41 100644 --- a/gateway/internal/adminapi/observability.go +++ b/gateway/internal/adminapi/observability.go @@ -832,9 +832,15 @@ func parseBucket(w http.ResponseWriter, r *http.Request, window time.Duration) ( } // parseDimensionParam reads ?dimension=… and enforces the set of -// dims phase 8 supports. agent-name, run-id, session-id, realm-id -// are metadata-backed; user-id is aliased to customer_id (see +// dims phase 8 supports. agent-name, run-id, session-id are +// metadata-backed; user-id is aliased to customer_id (see // dimensionValue). +// +// Phase 11 removed realm-id from the allow-list: every row in a +// swarm's logs.db is implicitly for that swarm's realm, so adding +// a redundant column would just confuse the dashboard. Cross-swarm +// analytics add the realm column at import time in the central +// aggregator. func parseDimensionParam(w http.ResponseWriter, r *http.Request) (string, bool) { q := r.URL.Query().Get("dimension") if q == "" { @@ -842,11 +848,11 @@ func parseDimensionParam(w http.ResponseWriter, r *http.Request) (string, bool) q = "agent-name" } switch q { - case "agent-name", "run-id", "session-id", "realm-id", "user-id": + case "agent-name", "run-id", "session-id", "user-id": return q, true default: writeError(w, http.StatusBadRequest, "bad_request", - "dimension must be one of: agent-name, run-id, session-id, realm-id, user-id") + "dimension must be one of: agent-name, run-id, session-id, user-id") return "", false } } @@ -880,7 +886,7 @@ func parsePagination(w http.ResponseWriter, r *http.Request) (int, int, bool) { // metadataFilterFromQuery is the common parser for the optional // `?user_id=` / `?agent_name=` / `?run_id=` / `?session_id=` / -// `?realm_id=` / `?org_id=` query params. Each maps to the matching +// `?org_id=` query params. Each maps to the matching // `metadata.` filter that Bifrost's /api/logs accepts. // // Why this exists in one place: every handler that rolls up @@ -891,6 +897,8 @@ func parsePagination(w http.ResponseWriter, r *http.Request) (int, int, bool) { // Empty values are skipped — `?user_id=` with no value doesn't add // a filter (so the SPA can pass `userID` directly without // short-circuiting on the empty string). +// +// Phase 11 dropped `?realm_id=` — see parseDimensionParam. func metadataFilterFromQuery(r *http.Request) map[string]string { q := r.URL.Query() out := map[string]string{} @@ -899,7 +907,6 @@ func metadataFilterFromQuery(r *http.Request) map[string]string { {"agent_name", "agent-name"}, {"run_id", "run-id"}, {"session_id", "session-id"}, - {"realm_id", "realm-id"}, {"org_id", "org-id"}, } for _, p := range pairs { diff --git a/gateway/internal/adminapi/trust.go b/gateway/internal/adminapi/trust.go index 1a3a20e8d..717a1f6a8 100644 --- a/gateway/internal/adminapi/trust.go +++ b/gateway/internal/adminapi/trust.go @@ -76,12 +76,16 @@ func (h *trustHandlers) upsert(w http.ResponseWriter, r *http.Request) { // dispatchPrefix routes everything under /_plugin/trust/ except the // status leaf. The trailing path is one of: // +// realm_id → PUT (set swarm self-identity, phase 11) // → GET or DELETE // /rotate → POST // // Anything else returns 404 (org_id with extra path segments) so we // don't silently accept malformed URLs that look like they should -// have done something. +// have done something. `realm_id` is a reserved path segment — orgs +// can't be named that, which is fine since validateRealmID's slash +// rejection mirrors validate's org_id rejection (so realm-ids can't +// collide with org-ids on the wire either). func (h *trustHandlers) dispatchPrefix(w http.ResponseWriter, r *http.Request) { rest := strings.TrimPrefix(r.URL.Path, trustPrefixPath) if rest == "" || rest == "status" { @@ -96,6 +100,14 @@ func (h *trustHandlers) dispatchPrefix(w http.ResponseWriter, r *http.Request) { return } + // /_plugin/trust/realm_id is the swarm-self-identity endpoint + // (phase 11). Handled out-of-band from the org-id dispatch + // because the URL segment is fixed, not a variable. + if rest == "realm_id" { + h.realmID(w, r) + return + } + parts := strings.Split(rest, "/") switch len(parts) { case 1: @@ -113,6 +125,37 @@ func (h *trustHandlers) dispatchPrefix(w http.ResponseWriter, r *http.Request) { } } +// realmID handles PUT /_plugin/trust/realm_id — set the swarm's own +// realm identity (phase 11). Empty value clears it (single-swarm +// deployment). Bearer-only (same auth model as other trust +// mutations); the SPA never writes this. +// +// Hive's reconciler is the typical caller, setting this at +// workspace provisioning for multi-swarm deployments. Operators can +// also set it by hand for diagnostics. +func (h *trustHandlers) realmID(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPut { + methodNotAllowed(w, http.MethodPut) + return + } + var req trust.RealmIDRequest + if err := decodeJSON(r, &req); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + v, err := h.reg.SetRealmID(req.RealmID) + if err != nil { + if errors.Is(err, trust.ErrInvalidRealmID) { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + pluginlog.Errf("adminapi: trust set realm_id: %v", err) + http.Error(w, "internal error", http.StatusInternalServerError) + return + } + writeJSON(w, http.StatusOK, trust.RealmIDResponse{OK: true, RealmID: v}) +} + // byOrg dispatches GET / DELETE on /_plugin/trust/. func (h *trustHandlers) byOrg(w http.ResponseWriter, r *http.Request, orgID string) { switch r.Method { diff --git a/gateway/internal/adminapi/trust_test.go b/gateway/internal/adminapi/trust_test.go index 8a1b138e6..9f0156815 100644 --- a/gateway/internal/adminapi/trust_test.go +++ b/gateway/internal/adminapi/trust_test.go @@ -296,6 +296,94 @@ func TestTrust_MethodEnforcement(t *testing.T) { resp.Body.Close() } +// ─── phase 11: realm_id endpoint ────────────────────────────────────── + +func TestTrust_RealmID_PutSetsAndStatusReturns(t *testing.T) { + srv, reg := newTestServer(t) + defer srv.Close() + + resp := do(t, srv, http.MethodPut, "/_plugin/trust/realm_id", + trust.RealmIDRequest{RealmID: "w1"}, true) + if resp.StatusCode != http.StatusOK { + t.Fatalf("put realm_id: %d", resp.StatusCode) + } + var put trust.RealmIDResponse + decode(t, resp, &put) + if !put.OK || put.RealmID != "w1" { + t.Fatalf("put response: %+v", put) + } + if reg.RealmID() != "w1" { + t.Fatalf("registry not updated: %q", reg.RealmID()) + } + + // /status surfaces it. + resp = do(t, srv, http.MethodGet, "/_plugin/trust/status", nil, true) + var st trust.StatusResponse + decode(t, resp, &st) + if st.RealmID != "w1" { + t.Fatalf("status realm_id: %+v", st) + } +} + +func TestTrust_RealmID_PutRejectsBadShape(t *testing.T) { + srv, _ := newTestServer(t) + defer srv.Close() + + resp := do(t, srv, http.MethodPut, "/_plugin/trust/realm_id", + trust.RealmIDRequest{RealmID: "has space"}, true) + if resp.StatusCode != http.StatusBadRequest { + t.Fatalf("want 400, got %d", resp.StatusCode) + } + resp.Body.Close() +} + +func TestTrust_RealmID_PutClears(t *testing.T) { + srv, reg := newTestServer(t) + defer srv.Close() + + _ = do(t, srv, http.MethodPut, "/_plugin/trust/realm_id", + trust.RealmIDRequest{RealmID: "w1"}, true).Body.Close() + + resp := do(t, srv, http.MethodPut, "/_plugin/trust/realm_id", + trust.RealmIDRequest{RealmID: ""}, true) + if resp.StatusCode != http.StatusOK { + t.Fatalf("clear: %d", resp.StatusCode) + } + resp.Body.Close() + if reg.RealmID() != "" { + t.Fatalf("expected cleared, got %q", reg.RealmID()) + } +} + +func TestTrust_RealmID_MethodEnforced(t *testing.T) { + srv, _ := newTestServer(t) + defer srv.Close() + + // GET is not allowed on the realm_id endpoint — its value is + // part of /_plugin/trust/status, so a dedicated GET would just + // duplicate that surface. + resp := do(t, srv, http.MethodGet, "/_plugin/trust/realm_id", nil, true) + if resp.StatusCode != http.StatusMethodNotAllowed { + t.Fatalf("want 405, got %d", resp.StatusCode) + } + resp.Body.Close() +} + +func TestTrust_RealmID_RequiresBearer(t *testing.T) { + srv, _ := newTestServer(t) + defer srv.Close() + + // PUT is a mutation → bearer-only via methodMuxedAuth (GET-only + // goes through cookieOrBearer; everything else goes through + // bearerOnly). Sending no auth must 401. + resp := do(t, srv, http.MethodPut, "/_plugin/trust/realm_id", + trust.RealmIDRequest{RealmID: "w1"}, false) + if resp.StatusCode != http.StatusUnauthorized { + t.Fatalf("want 401, got %d", resp.StatusCode) + } + resp.Body.Close() +} + // ─── 404 dispatch ───────────────────────────────────────────────────── func TestTrust_UnknownSubpath_404(t *testing.T) { diff --git a/gateway/internal/adminapi/ui/dist/index.html b/gateway/internal/adminapi/ui/dist/index.html index 813e9946e..58bdf76e9 100644 --- a/gateway/internal/adminapi/ui/dist/index.html +++ b/gateway/internal/adminapi/ui/dist/index.html @@ -5,7 +5,7 @@ Agent Mothership - + diff --git a/gateway/internal/adminapi/ui/src/api/queries.ts b/gateway/internal/adminapi/ui/src/api/queries.ts index facca0f03..241c5fb11 100644 --- a/gateway/internal/adminapi/ui/src/api/queries.ts +++ b/gateway/internal/adminapi/ui/src/api/queries.ts @@ -16,6 +16,7 @@ import type { SpendByAgentUserResponse, SpendByUserResponse, TrustOrg, + TrustStatus, UserDetailResponse, Window, Bucket, @@ -234,6 +235,25 @@ export function useTrustOrg(orgID: string | undefined) { }); } +// ─── /trust/status ────────────────────────────────────────────────── +// +// Surfaces the swarm's self-identity (`realm_id`) plus the trusted +// org list. The Provenance card on RunDetail reads `realm_id` to +// render "this swarm processes realm w1" — phase 11 moved the realm +// off per-row metadata and onto this single, signed-out status +// surface. Long stale time: the value changes only when an operator +// hits PUT /_plugin/trust/realm_id, which is human-scale (workspace +// provisioning, debugging). + +export function useTrustStatus() { + return useQuery({ + queryKey: ["trust", "status"], + queryFn: () => apiFetch("/trust/status"), + staleTime: 5 * 60_000, + retry: false, + }); +} + // ─── /runs/:id ────────────────────────────────────────────────────── // // Run detail is historical — no polling. The user explicitly hits diff --git a/gateway/internal/adminapi/ui/src/api/types.ts b/gateway/internal/adminapi/ui/src/api/types.ts index f8a1698e6..1b0720418 100644 --- a/gateway/internal/adminapi/ui/src/api/types.ts +++ b/gateway/internal/adminapi/ui/src/api/types.ts @@ -285,6 +285,20 @@ export interface TrustOrg { grace_until?: string; } +// Trust-registry status — mirrors gateway/internal/trust.StatusResponse. +// The Provenance card on RunDetail uses `realm_id` to show the +// swarm's self-identity ("this run was processed by swarm w1"), +// since phase 11 dropped the per-row realm-id metadata column. +export interface TrustStatus { + claimed: boolean; + org_count: number; + orgs: string[]; + seed_source: "" | "env" | "api"; + last_modified: string; + /** Set on multi-swarm deployments; absent / empty on single-swarm. */ + realm_id?: string; +} + // Per-agent budget (phase-8.5). Cap and the derived fields can be // null when no budget is configured for the agent — the UI renders // "no budget" rather than "$0". @@ -315,10 +329,12 @@ export type Window = "1h" | "6h" | "24h" | "7d" | "30d"; // note as Window. export type Bucket = "1m" | "5m" | "10m" | "1h" | "6h" | "1d"; -// Dimension values the histogram endpoint accepts. +// Dimension values the histogram endpoint accepts. Phase 11 removed +// `realm-id` — every row in a swarm's logs.db is implicitly for +// that swarm's realm, and the realm is surfaced on the trust-status +// card instead of as a per-row column. export type Dimension = | "agent-name" | "run-id" | "session-id" - | "realm-id" | "user-id"; diff --git a/gateway/internal/adminapi/ui/src/pages/RunDetail.tsx b/gateway/internal/adminapi/ui/src/pages/RunDetail.tsx index da54eaa84..4a4a92853 100644 --- a/gateway/internal/adminapi/ui/src/pages/RunDetail.tsx +++ b/gateway/internal/adminapi/ui/src/pages/RunDetail.tsx @@ -11,7 +11,7 @@ import { Link } from "wouter-preact"; import { DataTable } from "../components/tables/DataTable"; import { BotIcon, UserIcon } from "../components/icons"; import { getErrorMessage } from "../api/client"; -import { useRunCall, useRunDetail, useTrustOrg } from "../api/queries"; +import { useRunCall, useRunDetail, useTrustOrg, useTrustStatus } from "../api/queries"; import type { CacheDebug, CallDetailResponse, @@ -102,7 +102,6 @@ export function RunDetail({ runID }: Props) { const agent = md["agent-name"] ?? "—"; const user = md["user-id"] ?? "—"; - const realm = md["realm-id"] ?? "—"; const session = md["session-id"] ?? ""; const deployment = md["deployment"] ?? ""; const orgID = md["org-id"] ?? ""; @@ -112,6 +111,12 @@ export function RunDetail({ runID }: Props) { // "not in registry" badge); `undefined` is in-flight. const trust = useTrustOrg(orgID || undefined); + // Trust status carries the swarm's own realm_id (phase 11). Used + // by the AuthorizedBy banner to surface "processed by swarm w1" + // — the realm dropped off per-row metadata, so the registry + // status is the only place it lives now. + const trustStatus = useTrustStatus(); + return ( <>