@nullsquare/agent-authority 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/CONTRIBUTING.md +93 -0
  2. package/LICENSE +201 -0
  3. package/README.md +390 -0
  4. package/ROADMAP.md +149 -0
  5. package/SECURITY.md +116 -0
  6. package/docs/account-connections.md +173 -0
  7. package/docs/announcement-draft.md +13 -0
  8. package/docs/architecture.md +106 -0
  9. package/docs/assets/agent-authority-cover.svg +41 -0
  10. package/docs/clear-path.md +53 -0
  11. package/docs/cli.md +130 -0
  12. package/docs/evidence.md +143 -0
  13. package/docs/harness-bridge-mode.md +136 -0
  14. package/docs/harness-integration.md +223 -0
  15. package/docs/integration-contract.md +132 -0
  16. package/docs/integrations/vercel-ai-sdk.md +161 -0
  17. package/docs/launch-checklist.md +29 -0
  18. package/docs/npm-release.md +19 -0
  19. package/docs/openclaw-integration.md +97 -0
  20. package/docs/package-consumer-validation.md +18 -0
  21. package/docs/release-candidate-status.md +3 -0
  22. package/docs/release-guardrails.md +8 -0
  23. package/docs/release-notes-v0.4.md +26 -0
  24. package/docs/release-scope.md +3 -0
  25. package/docs/ship-criteria.md +3 -0
  26. package/docs/task-leases.md +253 -0
  27. package/docs/validation.md +124 -0
  28. package/examples/demo.js +19 -0
  29. package/examples/direct-guard.js +50 -0
  30. package/examples/harness-managed-connectors.js +72 -0
  31. package/examples/live-github-derived-mutation.js +208 -0
  32. package/examples/live-github-task-lease.js +80 -0
  33. package/examples/mission.json +20 -0
  34. package/examples/missions/chatgpt-web-validation.json +33 -0
  35. package/examples/openclaw-tool-wrapper.js +49 -0
  36. package/examples/task-lease-demo.js +98 -0
  37. package/examples/validation-mcp-upstream.js +112 -0
  38. package/package.json +80 -0
  39. package/src/agent-auth.js +135 -0
  40. package/src/approvals.js +157 -0
  41. package/src/cli.js +335 -0
  42. package/src/connections.js +203 -0
  43. package/src/execution.js +174 -0
  44. package/src/guard.js +79 -0
  45. package/src/harness-bridge.js +131 -0
  46. package/src/idempotency.js +118 -0
  47. package/src/index.js +291 -0
  48. package/src/integrations/ai-sdk.js +59 -0
  49. package/src/keys.js +15 -0
  50. package/src/mcp-gateway.js +142 -0
  51. package/src/mcp-remote.js +102 -0
  52. package/src/mcp-server.js +102 -0
  53. package/src/providers/github.js +149 -0
  54. package/src/runtime-env.js +53 -0
  55. package/src/sdk.js +75 -0
  56. package/src/server.js +146 -0
  57. package/src/storage.js +213 -0
  58. package/src/task-lease.js +266 -0
@@ -0,0 +1,135 @@
1
+ import { createHmac, randomUUID, timingSafeEqual } from 'node:crypto';
2
+
3
+ const ISSUER = 'agent-authority';
4
+ const AUDIENCE = 'agent-authority';
5
+
6
+ function b64url(value) {
7
+ return Buffer.from(typeof value === 'string' ? value : JSON.stringify(value)).toString('base64url');
8
+ }
9
+
10
+ function decodeJson(segment, label) {
11
+ try {
12
+ return JSON.parse(Buffer.from(segment, 'base64url').toString('utf8'));
13
+ } catch {
14
+ const error = new Error(`invalid ${label}`);
15
+ error.code = 'invalid_agent_token';
16
+ throw error;
17
+ }
18
+ }
19
+
20
+ function sign(key, signingInput) {
21
+ return createHmac('sha256', key).update(signingInput).digest('base64url');
22
+ }
23
+
24
+ function tokenError(code, message) {
25
+ const error = new Error(message);
26
+ error.code = code;
27
+ return error;
28
+ }
29
+
30
+ export function parseTtl(value, fallback = 3600) {
31
+ if (value === undefined || value === null || value === '') return fallback;
32
+ if (typeof value === 'number' || /^\d+$/.test(String(value))) return Number(value);
33
+ const match = String(value).trim().match(/^(\d+)(s|m|h|d)$/i);
34
+ if (!match) throw new Error('TTL must be seconds or a value such as 30m, 2h, or 1d');
35
+ const amount = Number(match[1]);
36
+ const unit = match[2].toLowerCase();
37
+ return amount * ({ s: 1, m: 60, h: 3600, d: 86400 }[unit]);
38
+ }
39
+
40
+ export function createAgentToken({
41
+ key,
42
+ principal_id,
43
+ agent_id,
44
+ mission_id = null,
45
+ capabilities = ['evaluate', 'prepare', 'execute', 'approval.read'],
46
+ ttl_seconds = 3600,
47
+ now = Date.now()
48
+ }) {
49
+ if (!key || Buffer.byteLength(key) < 32) throw new Error('agent signing key must contain at least 32 bytes');
50
+ if (!principal_id) throw new Error('principal_id is required');
51
+ if (!agent_id) throw new Error('agent_id is required');
52
+ const ttl = Number(ttl_seconds);
53
+ if (!Number.isFinite(ttl) || ttl < 1 || ttl > 86400) throw new Error('agent token TTL must be between 1 and 86400 seconds');
54
+
55
+ const issued = Math.floor(now / 1000);
56
+ const header = { alg: 'HS256', typ: 'AAUTH' };
57
+ const payload = {
58
+ v: 1,
59
+ iss: ISSUER,
60
+ aud: AUDIENCE,
61
+ sub: agent_id,
62
+ principal_id,
63
+ mission_id,
64
+ capabilities: [...new Set(capabilities)],
65
+ iat: issued,
66
+ exp: issued + ttl,
67
+ jti: randomUUID()
68
+ };
69
+
70
+ const encodedHeader = b64url(header);
71
+ const encodedPayload = b64url(payload);
72
+ const signingInput = `${encodedHeader}.${encodedPayload}`;
73
+ return `${signingInput}.${sign(key, signingInput)}`;
74
+ }
75
+
76
+ export function decodeAgentToken(token) {
77
+ const parts = String(token || '').split('.');
78
+ if (parts.length !== 3) throw tokenError('invalid_agent_token', 'agent token must contain three segments');
79
+ return { header: decodeJson(parts[0], 'agent token header'), payload: decodeJson(parts[1], 'agent token payload') };
80
+ }
81
+
82
+ export function verifyAgentToken(token, {
83
+ key,
84
+ principal_id,
85
+ mission = null,
86
+ mission_id = null,
87
+ capability = null,
88
+ now = Date.now()
89
+ } = {}) {
90
+ if (!key) throw new Error('agent signing key is required');
91
+ const parts = String(token || '').split('.');
92
+ if (parts.length !== 3) throw tokenError('invalid_agent_token', 'missing or malformed agent bearer token');
93
+ const [encodedHeader, encodedPayload, providedSignature] = parts;
94
+ const header = decodeJson(encodedHeader, 'agent token header');
95
+ const payload = decodeJson(encodedPayload, 'agent token payload');
96
+ if (header.alg !== 'HS256' || header.typ !== 'AAUTH') throw tokenError('invalid_agent_token', 'unsupported agent token format');
97
+
98
+ const expected = sign(key, `${encodedHeader}.${encodedPayload}`);
99
+ const expectedBuffer = Buffer.from(expected);
100
+ const providedBuffer = Buffer.from(providedSignature);
101
+ if (expectedBuffer.length !== providedBuffer.length || !timingSafeEqual(expectedBuffer, providedBuffer)) {
102
+ throw tokenError('invalid_agent_token', 'agent token signature is invalid');
103
+ }
104
+
105
+ const current = Math.floor(now / 1000);
106
+ if (payload.iss !== ISSUER || payload.aud !== AUDIENCE || payload.v !== 1) throw tokenError('invalid_agent_token', 'agent token issuer, audience, or version is invalid');
107
+ if (!payload.sub || !payload.principal_id || !payload.exp) throw tokenError('invalid_agent_token', 'agent token claims are incomplete');
108
+ if (current >= Number(payload.exp)) throw tokenError('agent_token_expired', 'agent token has expired');
109
+ if (Number(payload.iat) > current + 60) throw tokenError('invalid_agent_token', 'agent token was issued in the future');
110
+ if (principal_id && payload.principal_id !== principal_id) throw tokenError('principal_mismatch', 'agent token principal does not match this authority instance');
111
+
112
+ const expectedMissionId = mission?.mission_id || mission_id;
113
+ if (payload.mission_id && expectedMissionId && payload.mission_id !== expectedMissionId) {
114
+ throw tokenError('mission_binding_mismatch', 'agent token is bound to a different mission');
115
+ }
116
+ if (mission) {
117
+ if (mission.principal?.id !== payload.principal_id) throw tokenError('principal_mismatch', 'mission principal does not match agent token');
118
+ if (mission.agent?.id !== payload.sub) throw tokenError('agent_identity_mismatch', 'mission agent does not match authenticated agent instance');
119
+ }
120
+
121
+ if (capability) {
122
+ const capabilities = Array.isArray(payload.capabilities) ? payload.capabilities : [];
123
+ if (!capabilities.includes('*') && !capabilities.includes(capability)) {
124
+ throw tokenError('agent_capability_denied', `agent token does not permit ${capability}`);
125
+ }
126
+ }
127
+ return payload;
128
+ }
129
+
130
+ export function bearerToken(headers = {}) {
131
+ const value = typeof headers.get === 'function' ? headers.get('authorization') : headers.authorization;
132
+ const match = String(value || '').match(/^Bearer\s+(.+)$/i);
133
+ if (!match) throw tokenError('missing_agent_token', 'Authorization: Bearer <agent-token> is required');
134
+ return match[1].trim();
135
+ }
@@ -0,0 +1,157 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
3
+ import { dirname } from 'node:path';
4
+ import { hashObject } from './index.js';
5
+
6
+ function ensureDir(path) {
7
+ mkdirSync(path, { recursive: true, mode: 0o700 });
8
+ try { chmodSync(path, 0o700); } catch {}
9
+ }
10
+
11
+ function readJson(path) {
12
+ if (!existsSync(path)) return {};
13
+ return JSON.parse(readFileSync(path, 'utf8'));
14
+ }
15
+
16
+ function atomicJson(path, value) {
17
+ ensureDir(dirname(path));
18
+ const tmp = `${path}.${process.pid}.${randomUUID()}.tmp`;
19
+ writeFileSync(tmp, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
20
+ try { chmodSync(tmp, 0o600); } catch {}
21
+ renameSync(tmp, path);
22
+ try { chmodSync(path, 0o600); } catch {}
23
+ }
24
+
25
+ function approvalError(code, message) {
26
+ const error = new Error(message);
27
+ error.code = code;
28
+ return error;
29
+ }
30
+
31
+ function requestForFingerprint(request = {}) {
32
+ const { approval_id, ...rest } = request;
33
+ return rest;
34
+ }
35
+
36
+ export function approvalFingerprint(mission, request) {
37
+ return hashObject({
38
+ mission_hash: hashObject(mission),
39
+ request: requestForFingerprint(request)
40
+ });
41
+ }
42
+
43
+ function summarizeContext(context = {}) {
44
+ const summary = {};
45
+ for (const key of ['repository', 'account', 'resource', 'amount', 'currency', 'domain', 'zone', 'project']) {
46
+ if (context[key] !== undefined) summary[key] = context[key];
47
+ }
48
+ return summary;
49
+ }
50
+
51
+ export class JsonFileApprovalStore {
52
+ constructor(path, { defaultTtlSeconds = 600 } = {}) {
53
+ this.path = path;
54
+ this.defaultTtlSeconds = defaultTtlSeconds;
55
+ ensureDir(dirname(path));
56
+ }
57
+
58
+ all() { return readJson(this.path); }
59
+ write(value) { atomicJson(this.path, value); }
60
+
61
+ request({ mission, request, reason = 'human approval required', ttl_seconds = this.defaultTtlSeconds, now = Date.now() }) {
62
+ const fingerprint = approvalFingerprint(mission, request);
63
+ const all = this.all();
64
+ const currentMs = Number(now);
65
+ const existing = Object.values(all).find((record) =>
66
+ record.fingerprint === fingerprint &&
67
+ record.status === 'pending' &&
68
+ Date.parse(record.expires_at) > currentMs
69
+ );
70
+ if (existing) return { ...existing };
71
+
72
+ const approvalId = `approval:${randomUUID()}`;
73
+ const created = new Date(currentMs);
74
+ const expires = new Date(currentMs + Number(ttl_seconds) * 1000);
75
+ const record = {
76
+ approval_id: approvalId,
77
+ status: 'pending',
78
+ principal_id: mission.principal.id,
79
+ agent_id: mission.agent.id,
80
+ mission_id: mission.mission_id,
81
+ service: request.service,
82
+ action: request.action,
83
+ context: summarizeContext(request.context),
84
+ reason,
85
+ fingerprint,
86
+ created_at: created.toISOString(),
87
+ expires_at: expires.toISOString(),
88
+ decided_at: null,
89
+ decided_by: null,
90
+ consumed_at: null
91
+ };
92
+ all[approvalId] = record;
93
+ this.write(all);
94
+ return { ...record };
95
+ }
96
+
97
+ get(approvalId) {
98
+ const record = this.all()[approvalId];
99
+ return record ? { ...record } : null;
100
+ }
101
+
102
+ list({ principal_id, status } = {}) {
103
+ return Object.values(this.all())
104
+ .filter((record) => !principal_id || record.principal_id === principal_id)
105
+ .filter((record) => !status || record.status === status)
106
+ .sort((a, b) => String(b.created_at).localeCompare(String(a.created_at)))
107
+ .map((record) => ({ ...record }));
108
+ }
109
+
110
+ decide(approvalId, { principal_id, decision, now = Date.now() }) {
111
+ if (!['approved', 'denied'].includes(decision)) throw new Error('approval decision must be approved or denied');
112
+ const all = this.all();
113
+ const record = all[approvalId];
114
+ if (!record) throw approvalError('approval_not_found', 'approval request does not exist');
115
+ if (principal_id && record.principal_id !== principal_id) throw approvalError('principal_mismatch', 'approval belongs to another principal');
116
+ if (record.status !== 'pending') throw approvalError('approval_already_decided', `approval is already ${record.status}`);
117
+ if (Date.parse(record.expires_at) <= Number(now)) {
118
+ record.status = 'expired';
119
+ all[approvalId] = record;
120
+ this.write(all);
121
+ throw approvalError('approval_expired', 'approval request has expired');
122
+ }
123
+ record.status = decision;
124
+ record.decided_at = new Date(Number(now)).toISOString();
125
+ record.decided_by = principal_id || record.principal_id;
126
+ all[approvalId] = record;
127
+ this.write(all);
128
+ return { ...record };
129
+ }
130
+
131
+ approve(approvalId, options = {}) { return this.decide(approvalId, { ...options, decision: 'approved' }); }
132
+ deny(approvalId, options = {}) { return this.decide(approvalId, { ...options, decision: 'denied' }); }
133
+
134
+ consume(approvalId, { mission, request, now = Date.now() }) {
135
+ const all = this.all();
136
+ const record = all[approvalId];
137
+ if (!record) throw approvalError('approval_not_found', 'approval request does not exist');
138
+ if (Date.parse(record.expires_at) <= Number(now)) throw approvalError('approval_expired', 'approval request has expired');
139
+ if (record.status === 'denied') throw approvalError('approval_denied', 'human denied this action');
140
+ if (record.status === 'consumed' || record.consumed_at) {
141
+ throw approvalError('approval_replayed', 'approval has already been consumed');
142
+ }
143
+ if (record.status !== 'approved') throw approvalError('approval_pending', 'approval has not been granted yet');
144
+ if (record.principal_id !== mission.principal.id || record.agent_id !== mission.agent.id || record.mission_id !== mission.mission_id) {
145
+ throw approvalError('approval_binding_mismatch', 'approval is bound to a different principal, agent, or mission');
146
+ }
147
+ if (record.fingerprint !== approvalFingerprint(mission, request)) {
148
+ throw approvalError('approval_binding_mismatch', 'approval is bound to a different action or request context');
149
+ }
150
+
151
+ record.status = 'consumed';
152
+ record.consumed_at = new Date(Number(now)).toISOString();
153
+ all[approvalId] = record;
154
+ this.write(all);
155
+ return { ...record };
156
+ }
157
+ }
package/src/cli.js ADDED
@@ -0,0 +1,335 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync } from 'node:fs';
3
+ import { stdin as input } from 'node:process';
4
+ import { assertMission, evaluateMissionPolicy } from './index.js';
5
+ import { createAgentToken, parseTtl } from './agent-auth.js';
6
+ import { createRuntimeEnvironment } from './runtime-env.js';
7
+ import { authorityHome, ensureAuthorityHome, loadConfig, saveConfig } from './storage.js';
8
+
9
+ const VERSION = '0.3.0';
10
+
11
+ function fail(message, code = 1) {
12
+ console.error(`error: ${message}`);
13
+ process.exitCode = code;
14
+ }
15
+
16
+ function json(value) { console.log(JSON.stringify(value, null, 2)); }
17
+ function flag(args, name, fallback = undefined) {
18
+ const i = args.indexOf(name);
19
+ return i >= 0 && args[i + 1] !== undefined ? args[i + 1] : fallback;
20
+ }
21
+ function has(args, name) { return args.includes(name); }
22
+ function homeFrom(args) { return flag(args, '--home', process.env.AGENT_AUTHORITY_HOME || authorityHome()); }
23
+
24
+ async function readStdinSecret() {
25
+ if (process.stdin.isTTY) throw new Error('credential input must be piped on stdin; use `printf %s "$TOKEN" | agent-authority connect github --token-stdin`');
26
+ let value = '';
27
+ for await (const chunk of input) value += chunk;
28
+ value = value.trim();
29
+ if (!value) throw new Error('no credential received on stdin');
30
+ return value;
31
+ }
32
+
33
+ function help() {
34
+ console.log(`Agent Authority ${VERSION}
35
+
36
+ Usage:
37
+ agent-authority setup [--principal user:id] [--home PATH]
38
+ agent-authority status [--home PATH]
39
+ agent-authority doctor [--home PATH]
40
+ agent-authority config show [--home PATH]
41
+ agent-authority serve [--host HOST] [--port PORT] [--home PATH]
42
+
43
+ MCP gateway (read-only first milestone):
44
+ agent-authority mcp proxy --upstream URL --mission FILE [--service mcp:NAME] [--port 8790]
45
+
46
+ Connections:
47
+ agent-authority connections [--home PATH]
48
+ agent-authority connect github --token-stdin [--account ID] [--no-verify]
49
+ agent-authority disconnect github [--account ID]
50
+
51
+ Agent instances:
52
+ agent-authority agent token --agent AGENT_ID [--mission MISSION_ID] [--ttl 1h]
53
+ agent-authority agent token --agent AGENT_ID --admin [--ttl 15m]
54
+
55
+ Human approvals:
56
+ agent-authority approvals list [--status pending]
57
+ agent-authority approvals approve APPROVAL_ID
58
+ agent-authority approvals deny APPROVAL_ID
59
+
60
+ Missions:
61
+ agent-authority mission validate FILE
62
+ agent-authority mission evaluate FILE --service NAME --action NAME [--repository OWNER/REPO] [--context JSON]
63
+
64
+ Security:
65
+ Provider credentials are never accepted as command-line values.
66
+ Daemon /v1 APIs require short-lived signed agent-instance bearer tokens.
67
+ Human approvals are one-time and bound to the exact mission + request.
68
+ MCP proxy binds loopback only and exposes only tools explicitly declared read-only.
69
+
70
+ Environment:
71
+ AGENT_AUTHORITY_HOME Override ~/.agent-authority
72
+ AGENT_AUTHORITY_HOST Override serve host
73
+ AGENT_AUTHORITY_PORT Override serve port`);
74
+ }
75
+
76
+ async function setup(args) {
77
+ const home = homeFrom(args);
78
+ const principal = flag(args, '--principal', 'user:local');
79
+ ensureAuthorityHome({ home, principal_id: principal });
80
+ const config = loadConfig({ home });
81
+ if (config.principal_id !== principal) {
82
+ config.principal_id = principal;
83
+ saveConfig(config, { home });
84
+ }
85
+ const env = createRuntimeEnvironment({ home });
86
+ env.secrets.key();
87
+ console.log(`✓ Agent Authority initialized at ${home}`);
88
+ console.log(`Principal: ${principal}`);
89
+ console.log('Next: agent-authority doctor');
90
+ console.log('Then connect a provider and issue a short-lived token to an agent harness.');
91
+ }
92
+
93
+ function safeConnection(c) {
94
+ const { credential_ref, ...safe } = c;
95
+ return safe;
96
+ }
97
+
98
+ async function status(args) {
99
+ const home = homeFrom(args);
100
+ const env = createRuntimeEnvironment({ home });
101
+ const connections = env.broker.listConnections(env.config.principal_id);
102
+ const pendingApprovals = env.approvals.list({ principal_id: env.config.principal_id, status: 'pending' }).length;
103
+ json({
104
+ version: VERSION,
105
+ home,
106
+ principal_id: env.config.principal_id,
107
+ server: env.config.server,
108
+ connections,
109
+ pending_approvals: pendingApprovals,
110
+ api_authentication: 'agent-instance-bearer-token'
111
+ });
112
+ }
113
+
114
+ async function doctor(args) {
115
+ const home = homeFrom(args);
116
+ const checks = [];
117
+ try {
118
+ const env = createRuntimeEnvironment({ home });
119
+ checks.push({ check: 'config', ok: true, path: `${home}/config.json` });
120
+ env.secrets.key();
121
+ checks.push({ check: 'encrypted-vault', ok: true, path: env.config.paths.secrets });
122
+ checks.push({ check: 'agent-signing-key', ok: env.agentAuthKey.length === 32, path: env.agentAuthKeyPath });
123
+ checks.push({ check: 'connections-store', ok: true, count: env.broker.listConnections(env.config.principal_id).length });
124
+ checks.push({ check: 'approval-store', ok: true, pending: env.approvals.list({ principal_id: env.config.principal_id, status: 'pending' }).length });
125
+ checks.push({ check: 'loopback-default', ok: env.config.server.host === '127.0.0.1' || env.config.server.host === '::1', host: env.config.server.host });
126
+ } catch (error) {
127
+ checks.push({ check: 'runtime', ok: false, error: error.message });
128
+ }
129
+ for (const c of checks) console.log(`${c.ok ? '✓' : '✗'} ${c.check}${c.error ? `: ${c.error}` : ''}`);
130
+ if (checks.some((c) => !c.ok)) process.exitCode = 2;
131
+ }
132
+
133
+ async function configCommand(args) {
134
+ const sub = args.shift();
135
+ if (sub !== 'show') throw new Error('config command currently supports `show`');
136
+ const home = homeFrom(args);
137
+ json({ home, ...loadConfig({ home }) });
138
+ }
139
+
140
+ async function connectionsCommand(args) {
141
+ const home = homeFrom(args);
142
+ const env = createRuntimeEnvironment({ home });
143
+ json({ connections: env.broker.listConnections(env.config.principal_id) });
144
+ }
145
+
146
+ async function connectGitHub(args) {
147
+ if (!has(args, '--token-stdin')) throw new Error('GitHub currently requires --token-stdin; browser OAuth/PKCE is the next provider-onboarding milestone');
148
+ const home = homeFrom(args);
149
+ const env = createRuntimeEnvironment({ home });
150
+ const token = await readStdinSecret();
151
+ let accountId = flag(args, '--account');
152
+ let metadata = {};
153
+ let scopes = [];
154
+
155
+ if (!has(args, '--no-verify')) {
156
+ const response = await fetch('https://api.github.com/user', {
157
+ headers: { authorization: `Bearer ${token}`, accept: 'application/vnd.github+json', 'user-agent': 'agent-authority-cli' }
158
+ });
159
+ if (!response.ok) throw new Error(`GitHub credential verification failed (${response.status})`);
160
+ const profile = await response.json();
161
+ accountId ||= profile.login;
162
+ metadata = { login: profile.login, id: profile.id, html_url: profile.html_url };
163
+ const scopeHeader = response.headers.get('x-oauth-scopes');
164
+ if (scopeHeader) scopes = scopeHeader.split(',').map((s) => s.trim()).filter(Boolean);
165
+ }
166
+
167
+ accountId ||= 'default';
168
+ const connection = env.broker.connect({
169
+ principal_id: env.config.principal_id,
170
+ service: 'github',
171
+ account_id: accountId,
172
+ auth_kind: 'github-token',
173
+ credential: { access_token: token },
174
+ scopes,
175
+ metadata
176
+ });
177
+ console.log(`✓ GitHub connected as ${metadata.login || accountId}`);
178
+ json(safeConnection(connection));
179
+ }
180
+
181
+ async function disconnectGitHub(args) {
182
+ const home = homeFrom(args);
183
+ const env = createRuntimeEnvironment({ home });
184
+ const accountId = flag(args, '--account', 'default');
185
+ const result = env.broker.disconnect({ principal_id: env.config.principal_id, service: 'github', account_id: accountId });
186
+ if (!result) throw new Error(`GitHub account ${accountId} is not connected`);
187
+ console.log(`✓ GitHub ${accountId} disconnected and local credential removed`);
188
+ }
189
+
190
+ async function agentCommand(args) {
191
+ const sub = args.shift();
192
+ if (sub !== 'token') throw new Error('agent command currently supports `token`');
193
+ const home = homeFrom(args);
194
+ const env = createRuntimeEnvironment({ home });
195
+ const agentId = flag(args, '--agent');
196
+ if (!agentId) throw new Error('--agent AGENT_ID is required');
197
+ const missionId = flag(args, '--mission', null);
198
+ const ttl = parseTtl(flag(args, '--ttl', has(args, '--admin') ? '15m' : '1h'));
199
+ const capabilities = has(args, '--admin')
200
+ ? ['*']
201
+ : (flag(args, '--capabilities')
202
+ ? flag(args, '--capabilities').split(',').map((v) => v.trim()).filter(Boolean)
203
+ : ['evaluate', 'prepare', 'execute', 'approval.read']);
204
+ const token = createAgentToken({
205
+ key: env.agentAuthKey,
206
+ principal_id: env.config.principal_id,
207
+ agent_id: agentId,
208
+ mission_id: missionId,
209
+ capabilities,
210
+ ttl_seconds: ttl
211
+ });
212
+ if (has(args, '--json')) {
213
+ json({ token, agent_id: agentId, mission_id: missionId, ttl_seconds: ttl, capabilities });
214
+ } else {
215
+ console.log(token);
216
+ }
217
+ }
218
+
219
+ async function approvalsCommand(args) {
220
+ const sub = args.shift();
221
+ const home = homeFrom(args);
222
+ const env = createRuntimeEnvironment({ home });
223
+ if (sub === 'list') {
224
+ const statusFilter = flag(args, '--status');
225
+ return json({ approvals: env.approvals.list({ principal_id: env.config.principal_id, status: statusFilter }) });
226
+ }
227
+ const approvalId = args.shift();
228
+ if (!approvalId) throw new Error('approval id is required');
229
+ if (sub === 'approve') {
230
+ const result = env.approvals.approve(approvalId, { principal_id: env.config.principal_id });
231
+ console.log(`✓ approved ${approvalId}`);
232
+ return json(result);
233
+ }
234
+ if (sub === 'deny') {
235
+ const result = env.approvals.deny(approvalId, { principal_id: env.config.principal_id });
236
+ console.log(`✓ denied ${approvalId}`);
237
+ return json(result);
238
+ }
239
+ throw new Error('approvals command must be `list`, `approve`, or `deny`');
240
+ }
241
+
242
+ function loadMission(path) {
243
+ if (!path) throw new Error('mission file is required');
244
+ return JSON.parse(readFileSync(path, 'utf8'));
245
+ }
246
+
247
+ async function missionCommand(args) {
248
+ const sub = args[0];
249
+ const file = args[1];
250
+ if (sub === 'validate') {
251
+ const mission = assertMission(loadMission(file));
252
+ json({ ok: true, mission_id: mission.mission_id });
253
+ return;
254
+ }
255
+ if (sub === 'evaluate') {
256
+ const mission = loadMission(file);
257
+ const service = flag(args, '--service');
258
+ const action = flag(args, '--action');
259
+ const repository = flag(args, '--repository');
260
+ const rawContext = flag(args, '--context');
261
+ const context = rawContext ? JSON.parse(rawContext) : {};
262
+ if (repository) context.repository = repository;
263
+ const result = evaluateMissionPolicy(mission, { service, action, context });
264
+ json(result);
265
+ if (result.decision === 'deny') process.exitCode = 3;
266
+ return;
267
+ }
268
+ throw new Error('mission command must be `validate` or `evaluate`');
269
+ }
270
+
271
+ async function mcpCommand(args) {
272
+ const sub = args.shift();
273
+ if (sub !== 'proxy') throw new Error('mcp command currently supports `proxy`');
274
+ const upstreamUrl = flag(args, '--upstream');
275
+ const missionPath = flag(args, '--mission');
276
+ if (!upstreamUrl) throw new Error('--upstream URL is required');
277
+ if (!missionPath) throw new Error('--mission FILE is required');
278
+
279
+ const home = homeFrom(args);
280
+ const env = createRuntimeEnvironment({ home });
281
+ const mission = assertMission(loadMission(missionPath));
282
+ if (mission.principal.id !== env.config.principal_id) {
283
+ throw new Error(`mission principal ${mission.principal.id} does not match local principal ${env.config.principal_id}`);
284
+ }
285
+
286
+ const host = flag(args, '--host', '127.0.0.1');
287
+ const port = Number(flag(args, '--port', '8790'));
288
+ const service = flag(args, '--service', 'mcp:upstream');
289
+ const { startMcpProxyServer } = await import('./mcp-server.js');
290
+ const instance = await startMcpProxyServer({
291
+ mission,
292
+ runtime: env.runtime,
293
+ upstreamUrl,
294
+ service,
295
+ host,
296
+ port
297
+ });
298
+ console.log(`✓ Agent Authority MCP gateway listening on http://${instance.host}:${instance.port}/mcp`);
299
+ console.log(`Mission: ${mission.mission_id}`);
300
+ console.log(`Service: ${service}`);
301
+ console.log(`Upstream: ${upstreamUrl}`);
302
+ console.log('Mode: read-only (write tools are not advertised or callable)');
303
+ return instance;
304
+ }
305
+
306
+ async function main() {
307
+ const args = process.argv.slice(2);
308
+ const command = args.shift();
309
+ if (!command || command === 'help' || command === '--help' || command === '-h') return help();
310
+ if (command === '--version' || command === 'version') return console.log(VERSION);
311
+ if (command === 'setup' || command === 'init') return setup(args);
312
+ if (command === 'status') return status(args);
313
+ if (command === 'doctor') return doctor(args);
314
+ if (command === 'config') return configCommand(args);
315
+ if (command === 'connections') return connectionsCommand(args);
316
+ if (command === 'agent') return agentCommand(args);
317
+ if (command === 'approvals') return approvalsCommand(args);
318
+ if (command === 'mission') return missionCommand(args);
319
+ if (command === 'mcp') return mcpCommand(args);
320
+ if (command === 'connect') {
321
+ if (args.shift() !== 'github') throw new Error('only the GitHub native connection is implemented today');
322
+ return connectGitHub(args);
323
+ }
324
+ if (command === 'disconnect') {
325
+ if (args.shift() !== 'github') throw new Error('only the GitHub native connection is implemented today');
326
+ return disconnectGitHub(args);
327
+ }
328
+ if (command === 'serve' || command === 'start') {
329
+ const { startServer } = await import('./server.js');
330
+ return startServer({ home: homeFrom(args), host: flag(args, '--host'), port: flag(args, '--port') ? Number(flag(args, '--port')) : undefined });
331
+ }
332
+ throw new Error(`unknown command: ${command}`);
333
+ }
334
+
335
+ main().catch((error) => fail(error.message));