@nullsquare/agent-authority 0.4.5 → 0.4.7

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.
@@ -0,0 +1,223 @@
1
+ import { AuthorityApprovalRequiredError, AuthorityDeniedError } from '../src/guard.js';
2
+ import { createTask } from '../src/task.js';
3
+
4
+ const ticketId = 'ticket:4821';
5
+
6
+ function operationExtractor({ service, action, selector, extractorId, validate }) {
7
+ return ({ receipt, output } = {}) => {
8
+ if (receipt?.service !== service || receipt?.action !== action) {
9
+ const error = new Error(`${extractorId} received the wrong operation`);
10
+ error.code = 'trusted_extractor_operation_mismatch';
11
+ throw error;
12
+ }
13
+ const value = selector.split('.').slice(1).reduce((current, key) => current?.[key], output);
14
+ if (!validate(value)) {
15
+ const error = new Error(`${extractorId} received invalid normalized output`);
16
+ error.code = 'trusted_extractor_output_invalid';
17
+ throw error;
18
+ }
19
+ return { extractor_id: extractorId, selector };
20
+ };
21
+ }
22
+
23
+ const orderIdExtractor = operationExtractor({
24
+ service: 'helpdesk',
25
+ action: 'ticket.read',
26
+ selector: 'output.order_id',
27
+ extractorId: 'demo.helpdesk.ticket.order-id.v1',
28
+ validate: (value) => typeof value === 'string' && value.startsWith('order:')
29
+ });
30
+
31
+ const paymentIdExtractor = operationExtractor({
32
+ service: 'commerce',
33
+ action: 'order.read',
34
+ selector: 'output.payment_id',
35
+ extractorId: 'demo.commerce.order.payment-id.v1',
36
+ validate: (value) => typeof value === 'string' && value.startsWith('payment:')
37
+ });
38
+
39
+ const paymentAmountExtractor = operationExtractor({
40
+ service: 'payments',
41
+ action: 'payment.read',
42
+ selector: 'output.amount_minor',
43
+ extractorId: 'demo.payments.payment.amount-minor.v1',
44
+ validate: (value) => Number.isSafeInteger(value) && value >= 0
45
+ });
46
+
47
+ const paymentCurrencyExtractor = operationExtractor({
48
+ service: 'payments',
49
+ action: 'payment.read',
50
+ selector: 'output.currency',
51
+ extractorId: 'demo.payments.payment.currency.v1',
52
+ validate: (value) => typeof value === 'string' && /^[A-Z]{3}$/.test(value)
53
+ });
54
+
55
+ const task = createTask({
56
+ principal: 'user:finance-demo',
57
+ agent: 'agent:finance-demo',
58
+ request: 'Resolve this support ticket by refunding only its affected payment',
59
+ permissions: {
60
+ helpdesk: {
61
+ allow: ['ticket.read'],
62
+ constraints: { ticket_id: [ticketId] }
63
+ },
64
+ commerce: {
65
+ allow: ['order.read'],
66
+ constraints: {}
67
+ },
68
+ payments: {
69
+ allow: ['payment.read', 'refund.create'],
70
+ deny: ['payment.capture', 'refund.delete'],
71
+ constraints: {}
72
+ }
73
+ },
74
+ authority: {
75
+ ticket: { kind: 'helpdesk.ticket', value: ticketId }
76
+ },
77
+ bindings: [
78
+ { service: 'helpdesk', action: 'ticket.read', field: 'ticket_id', authority: 'ticket' },
79
+ { service: 'commerce', action: 'order.read', field: 'order_id', authority: 'order' },
80
+ { service: 'payments', action: 'payment.read', field: 'payment_id', authority: 'payment' },
81
+ { service: 'payments', action: 'refund.create', field: 'payment_id', authority: 'payment' },
82
+ { service: 'payments', action: 'refund.create', field: 'amount_minor', authority: 'paymentAmount' },
83
+ { service: 'payments', action: 'refund.create', field: 'currency', authority: 'paymentCurrency' }
84
+ ]
85
+ });
86
+
87
+ let ticketReads = 0;
88
+ let orderReads = 0;
89
+ let paymentReads = 0;
90
+ let refunds = 0;
91
+
92
+ console.log('Task: Resolve one support ticket by refunding only the payment discovered through that ticket');
93
+
94
+ console.log('1. Read the exact authorized ticket');
95
+ const ticket = await task.run({
96
+ service: 'helpdesk',
97
+ action: 'ticket.read',
98
+ context: { ticket_id: ticketId }
99
+ }, async () => {
100
+ ticketReads += 1;
101
+ return { ticket_id: ticketId, customer_id: 'customer:77', order_id: 'order:991' };
102
+ });
103
+
104
+ const order = task.authorityFrom(ticket, {
105
+ name: 'order',
106
+ kind: 'commerce.order',
107
+ from: 'ticket',
108
+ extractor: orderIdExtractor
109
+ });
110
+ console.log(` authority -> ${order.value}`);
111
+
112
+ console.log('2. Read only the order established by the ticket');
113
+ const orderRead = await task.run({
114
+ service: 'commerce',
115
+ action: 'order.read',
116
+ context: { order_id: order.value }
117
+ }, async () => {
118
+ orderReads += 1;
119
+ return { order_id: order.value, payment_id: 'payment:abc123' };
120
+ });
121
+
122
+ const payment = task.authorityFrom(orderRead, {
123
+ name: 'payment',
124
+ kind: 'payments.payment',
125
+ from: 'order',
126
+ extractor: paymentIdExtractor
127
+ });
128
+ console.log(` authority -> ${payment.value}`);
129
+
130
+ console.log('3. Read only the payment established by the order');
131
+ const paymentRead = await task.run({
132
+ service: 'payments',
133
+ action: 'payment.read',
134
+ context: { payment_id: payment.value }
135
+ }, async () => {
136
+ paymentReads += 1;
137
+ return { payment_id: payment.value, amount_minor: 12500, currency: 'USD' };
138
+ });
139
+
140
+ const amount = task.authorityFrom(paymentRead, {
141
+ name: 'paymentAmount',
142
+ kind: 'money.minor-units',
143
+ from: 'payment',
144
+ extractor: paymentAmountExtractor
145
+ });
146
+ const currency = task.authorityFrom(paymentRead, {
147
+ name: 'paymentCurrency',
148
+ kind: 'money.currency',
149
+ from: 'payment',
150
+ extractor: paymentCurrencyExtractor
151
+ });
152
+ console.log(` authority -> ${amount.value} minor units ${currency.value}`);
153
+
154
+ console.log('4. Refund exactly the payment established by the authorized chain');
155
+ await task.run({
156
+ service: 'payments',
157
+ action: 'refund.create',
158
+ context: {
159
+ payment_id: payment.value,
160
+ amount_minor: amount.value,
161
+ currency: currency.value
162
+ }
163
+ }, async () => {
164
+ refunds += 1;
165
+ return { refund_id: 'refund:full-1', status: 'succeeded' };
166
+ });
167
+ console.log(' ALLOW -> exact full refund executed');
168
+
169
+ async function proveRefundBlocked(label, context) {
170
+ try {
171
+ await task.run({ service: 'payments', action: 'refund.create', context }, async () => {
172
+ refunds += 1;
173
+ return { refund_id: 'must-not-exist' };
174
+ });
175
+ throw new Error(`${label} unexpectedly executed`);
176
+ } catch (error) {
177
+ if (!(error instanceof AuthorityApprovalRequiredError) || error.code !== 'authority_delta_required') {
178
+ throw error;
179
+ }
180
+ console.log(` STEP-UP -> ${label} blocked before refund callback`);
181
+ console.log(` ${task.explain(error).summary}`);
182
+ }
183
+ }
184
+
185
+ console.log('5. Prove unrelated payment and amount changes do not inherit the task authority');
186
+ await proveRefundBlocked('unrelated payment', {
187
+ payment_id: 'payment:other',
188
+ amount_minor: amount.value,
189
+ currency: currency.value
190
+ });
191
+ await proveRefundBlocked('over-refund', {
192
+ payment_id: payment.value,
193
+ amount_minor: 15000,
194
+ currency: currency.value
195
+ });
196
+ await proveRefundBlocked('partial refund under the current exact-binding model', {
197
+ payment_id: payment.value,
198
+ amount_minor: 5000,
199
+ currency: currency.value
200
+ });
201
+
202
+ if (refunds !== 1) throw new Error(`expected exactly one refund callback, got ${refunds}`);
203
+
204
+ console.log('6. Complete the task and remove the remaining refund authority');
205
+ task.complete('ticket refund completed');
206
+ try {
207
+ await task.run({
208
+ service: 'payments',
209
+ action: 'refund.create',
210
+ context: { payment_id: payment.value, amount_minor: amount.value, currency: currency.value }
211
+ }, async () => {
212
+ refunds += 1;
213
+ return { refund_id: 'must-not-run-after-completion' };
214
+ });
215
+ throw new Error('post-completion refund unexpectedly executed');
216
+ } catch (error) {
217
+ if (!(error instanceof AuthorityDeniedError) || error.code !== 'task_lease_completed') throw error;
218
+ console.log(' DENY -> completed task cannot refund again');
219
+ }
220
+
221
+ console.log(`Provider-shaped callbacks: tickets=${ticketReads}, orders=${orderReads}, payments=${paymentReads}, refunds=${refunds}`);
222
+ console.log('PASS -> ticket -> order -> payment -> exact refund authority stayed on one evidence-derived lineage');
223
+ console.log('PRODUCT GAP -> partial refunds currently step up because Task Lease bindings are exact equality; derived numeric <= is intentionally not implemented yet');
@@ -0,0 +1,102 @@
1
+ import { createTask } from '../src/task.js';
2
+ import { AuthorityApprovalRequiredError } from '../src/guard.js';
3
+
4
+ const repository = 'Null-Square/agent-authority';
5
+
6
+ function issueNumberExtractor({ receipt, output } = {}) {
7
+ if (receipt?.service !== 'github' || receipt?.action !== 'issue.list') {
8
+ const error = new Error('extractor only accepts github:issue.list');
9
+ error.code = 'trusted_extractor_operation_mismatch';
10
+ throw error;
11
+ }
12
+ if (!Number.isSafeInteger(output?.selected_issue_number)) {
13
+ const error = new Error('discovery did not produce a canonical issue number');
14
+ error.code = 'trusted_extractor_output_invalid';
15
+ throw error;
16
+ }
17
+ return {
18
+ extractor_id: 'demo.github.selected-issue.v1',
19
+ selector: 'output.selected_issue_number'
20
+ };
21
+ }
22
+
23
+ const task = createTask({
24
+ principal: 'user:demo',
25
+ agent: 'agent:demo',
26
+ request: 'Find issue #42 and leave one comment only on that issue',
27
+ permissions: {
28
+ github: {
29
+ allow: ['issue.list', 'issue.comment'],
30
+ deny: ['issue.close', 'repo.delete'],
31
+ constraints: { repository: [repository] }
32
+ }
33
+ },
34
+ authority: {
35
+ repository: { kind: 'github.repository', value: repository }
36
+ },
37
+ bindings: [
38
+ { service: 'github', action: 'issue.list', field: 'repository', authority: 'repository' },
39
+ { service: 'github', action: 'issue.comment', field: 'repository', authority: 'repository' }
40
+ ]
41
+ });
42
+
43
+ let providerEffects = 0;
44
+
45
+ console.log('Task: Find issue #42 and leave one comment only on that issue');
46
+ console.log('1. Discover the task resource through an authorized read');
47
+ const discovery = await task.run({
48
+ service: 'github',
49
+ action: 'issue.list',
50
+ context: { repository }
51
+ }, async () => {
52
+ providerEffects += 1;
53
+ // Replace this callback with your existing GitHub SDK/provider call.
54
+ return { selected_issue_number: 42, selected_issue_title: 'Example issue' };
55
+ });
56
+
57
+ console.log(` ALLOW -> discovered issue #${discovery.output.selected_issue_number}`);
58
+
59
+ const issue = task.authorityFrom(discovery, {
60
+ name: 'issue',
61
+ kind: 'github.issue.number',
62
+ from: 'repository',
63
+ extractor: issueNumberExtractor
64
+ });
65
+
66
+ task.bind({
67
+ service: 'github',
68
+ action: 'issue.comment',
69
+ field: 'issue_number',
70
+ authority: 'issue'
71
+ });
72
+
73
+ console.log(`2. Authority follows the guarded result -> issue #${issue.value}`);
74
+
75
+ await task.run({
76
+ service: 'github',
77
+ action: 'issue.comment',
78
+ context: { repository, issue_number: issue.value, body: 'Handled by the task.' }
79
+ }, async () => {
80
+ providerEffects += 1;
81
+ return { comment_id: 1001 };
82
+ });
83
+ console.log('3. ALLOW -> comment on issue #42 executed');
84
+
85
+ try {
86
+ await task.run({
87
+ service: 'github',
88
+ action: 'issue.comment',
89
+ context: { repository, issue_number: 7, body: 'This must not execute.' }
90
+ }, async () => {
91
+ providerEffects += 1;
92
+ return { comment_id: 1002 };
93
+ });
94
+ } catch (error) {
95
+ if (!(error instanceof AuthorityApprovalRequiredError)) throw error;
96
+ const explanation = task.explain(error);
97
+ console.log('4. STEP-UP -> unrelated issue blocked before the provider callback');
98
+ console.log(` ${explanation.summary}`);
99
+ }
100
+
101
+ console.log(`Provider effects executed: ${providerEffects} (expected: 2)`);
102
+ console.log('PASS -> useful task actions proceed while unrelated account authority does not become task authority');
@@ -0,0 +1,127 @@
1
+ import { AuthorityApprovalRequiredError, AuthorityDeniedError } from '../src/guard.js';
2
+ import { gmailThreadSenderAuthorityExtractor } from '../src/providers/google.js';
3
+ import { createTask } from '../src/task.js';
4
+
5
+ const threadId = 'thread:demo-91';
6
+ const calendarId = 'primary';
7
+
8
+ const task = createTask({
9
+ principal: 'user:demo',
10
+ agent: 'agent:support-demo',
11
+ request: 'Handle the customer request in this email thread and schedule the requested meeting',
12
+ permissions: {
13
+ gmail: {
14
+ allow: ['thread.read'],
15
+ deny: ['email.delete'],
16
+ constraints: { thread_id: [threadId] }
17
+ },
18
+ calendar: {
19
+ allow: ['event.create'],
20
+ deny: ['event.delete'],
21
+ constraints: { calendar_id: [calendarId] }
22
+ }
23
+ },
24
+ authority: {
25
+ originThread: { kind: 'gmail.thread', value: threadId },
26
+ calendar: { kind: 'calendar.id', value: calendarId }
27
+ },
28
+ bindings: [
29
+ { service: 'gmail', action: 'thread.read', field: 'thread_id', authority: 'originThread' },
30
+ { service: 'calendar', action: 'event.create', field: 'calendar_id', authority: 'calendar' }
31
+ ]
32
+ });
33
+
34
+ let gmailReads = 0;
35
+ let calendarMutations = 0;
36
+
37
+ console.log('Task: Handle one customer email and schedule only the meeting justified by that thread');
38
+ console.log('1. Read the exact task-authorized Gmail thread');
39
+ const read = await task.run({
40
+ service: 'gmail',
41
+ action: 'thread.read',
42
+ context: { thread_id: threadId }
43
+ }, async () => {
44
+ gmailReads += 1;
45
+ // Replace with the existing Gmail adapter/SDK call. This shape matches the
46
+ // normalized output produced by createGoogleProviderAdapter().
47
+ return {
48
+ provider: 'gmail',
49
+ sender_email: 'customer@example.com',
50
+ thread_id: threadId,
51
+ message_count: 1
52
+ };
53
+ });
54
+ console.log(` ALLOW -> sender discovered: ${read.output.sender_email}`);
55
+
56
+ const customer = task.authorityFrom(read, {
57
+ name: 'customerEmail',
58
+ kind: 'email.address',
59
+ from: 'originThread',
60
+ extractor: gmailThreadSenderAuthorityExtractor
61
+ });
62
+
63
+ task.bind({
64
+ service: 'calendar',
65
+ action: 'event.create',
66
+ field: 'attendee_email',
67
+ authority: 'customerEmail'
68
+ });
69
+ console.log(`2. Authority follows the guarded Gmail result -> ${customer.value}`);
70
+
71
+ const meetingContext = {
72
+ calendar_id: calendarId,
73
+ attendee_email: customer.value,
74
+ start_time: '2030-01-15T10:00:00Z',
75
+ end_time: '2030-01-15T10:30:00Z'
76
+ };
77
+
78
+ const meeting = await task.run({
79
+ service: 'calendar',
80
+ action: 'event.create',
81
+ context: meetingContext
82
+ }, async () => {
83
+ calendarMutations += 1;
84
+ return { provider: 'calendar', event_id: 'event:customer-demo' };
85
+ });
86
+ console.log(`3. ALLOW -> Calendar event ${meeting.output.event_id} for ${customer.value}`);
87
+
88
+ try {
89
+ await task.run({
90
+ service: 'calendar',
91
+ action: 'event.create',
92
+ context: { ...meetingContext, attendee_email: 'other@example.com' }
93
+ }, async () => {
94
+ calendarMutations += 1;
95
+ return { event_id: 'must-not-exist' };
96
+ });
97
+ throw new Error('unrelated attendee unexpectedly executed');
98
+ } catch (error) {
99
+ if (!(error instanceof AuthorityApprovalRequiredError) || error.code !== 'authority_delta_required') {
100
+ throw error;
101
+ }
102
+ console.log('4. STEP-UP -> unrelated attendee blocked before Calendar mutation');
103
+ console.log(` ${task.explain(error).summary}`);
104
+ }
105
+
106
+ if (calendarMutations !== 1) {
107
+ throw new Error(`expected exactly one Calendar mutation, got ${calendarMutations}`);
108
+ }
109
+
110
+ task.complete('customer request handled');
111
+ try {
112
+ await task.run({
113
+ service: 'calendar',
114
+ action: 'event.create',
115
+ context: meetingContext
116
+ }, async () => {
117
+ calendarMutations += 1;
118
+ return { event_id: 'must-not-run-after-completion' };
119
+ });
120
+ throw new Error('post-completion Calendar mutation unexpectedly executed');
121
+ } catch (error) {
122
+ if (!(error instanceof AuthorityDeniedError) || error.code !== 'task_lease_completed') throw error;
123
+ console.log('5. DENY -> task completion removed the meeting authority');
124
+ }
125
+
126
+ console.log(`Provider-shaped callbacks: gmail_reads=${gmailReads}, calendar_mutations=${calendarMutations}`);
127
+ console.log('PASS -> one authorized email established exactly one downstream Calendar attendee');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nullsquare/agent-authority",
3
- "version": "0.4.5",
3
+ "version": "0.4.7",
4
4
  "description": "Task-bounded authority runtime for AI agents: give agents tasks, not standing account permissions.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -32,18 +32,21 @@
32
32
  "setup": "node src/cli.js setup",
33
33
  "doctor": "node src/cli.js doctor",
34
34
  "demo": "node examples/demo.js",
35
+ "demo:task": "node examples/task-first-github.js",
35
36
  "demo:task-lease": "node examples/task-lease-demo.js",
36
37
  "demo:live-github": "node examples/live-github-task-lease.js",
37
38
  "demo:live-derived-github": "node examples/live-github-derived-mutation.js",
38
39
  "demo:live-google": "node examples/live-google-cross-provider.js",
39
40
  "demo:mcp-upstream": "node examples/validation-mcp-upstream.js",
40
41
  "demo:guard": "node examples/direct-guard.js",
42
+ "demo:connected-github": "node examples/quickstart-github-connected.mjs",
43
+ "benchmark:task": "node benchmarks/task-utility.mjs",
41
44
  "test": "node --test test/*.test.js",
42
45
  "test:ai-sdk": "node --test test/integrations/ai-sdk.integration.mjs",
43
46
  "test:coverage": "node --experimental-test-coverage --test test/*.test.js",
44
- "check:syntax": "node --check src/index.js && node --check src/authority-evidence.js && node --check src/connections.js && node --check src/execution.js && node --check src/providers/github.js && node --check src/providers/google.js && node --check src/storage.js && node --check src/durable-task-lease.js && node --check src/runtime-env.js && node --check src/sdk.js && node --check src/server.js && node --check src/cli.js && node --check src/agent-auth.js && node --check src/approvals.js && node --check src/idempotency.js && node --check src/keys.js && node --check src/harness-bridge.js && node --check src/guard.js && node --check src/task-lease.js && node --check src/mcp-gateway.js && node --check src/mcp-remote.js && node --check src/mcp-server.js && node --check src/integrations/ai-sdk.js && node --check examples/validation-mcp-upstream.js && node --check examples/direct-guard.js && node --check examples/task-lease-demo.js && node --check examples/live-github-task-lease.js && node --check examples/live-github-derived-mutation.js && node --check examples/live-google-cross-provider.js",
47
+ "check:syntax": "node --check src/index.js && node --check src/authority-evidence.js && node --check src/connections.js && node --check src/execution.js && node --check src/providers/github.js && node --check src/providers/google.js && node --check src/storage.js && node --check src/durable-task-lease.js && node --check src/task.js && node --check src/runtime-env.js && node --check src/sdk.js && node --check src/server.js && node --check src/cli.js && node --check src/agent-auth.js && node --check src/approvals.js && node --check src/idempotency.js && node --check src/keys.js && node --check src/harness-bridge.js && node --check src/guard.js && node --check src/task-lease.js && node --check src/mcp-gateway.js && node --check src/mcp-remote.js && node --check src/mcp-server.js && node --check src/integrations/ai-sdk.js && node --check examples/validation-mcp-upstream.js && node --check examples/direct-guard.js && node --check examples/task-first-github.js && node --check examples/task-lease-demo.js && node --check examples/live-github-task-lease.js && node --check examples/live-github-derived-mutation.js && node --check examples/live-google-cross-provider.js && node --check examples/quickstart-github-connected.mjs && node --check benchmarks/task-utility.mjs",
45
48
  "check:package": "npm pack --dry-run",
46
- "check": "npm run check:syntax && npm test && npm run demo:task-lease && npm run check:package"
49
+ "check": "npm run check:syntax && npm test && npm run demo:task && npm run benchmark:task && npm run demo:task-lease && npm run check:package"
47
50
  },
48
51
  "dependencies": {
49
52
  "@modelcontextprotocol/client": "^2.0.0",
@@ -71,13 +74,15 @@
71
74
  "./mcp-gateway": "./src/mcp-gateway.js",
72
75
  "./mcp-remote": "./src/mcp-remote.js",
73
76
  "./mcp-server": "./src/mcp-server.js",
77
+ "./runtime-env": "./src/runtime-env.js",
74
78
  "./sdk": "./src/sdk.js",
75
79
  "./storage": "./src/storage.js",
80
+ "./task": "./src/task.js",
76
81
  "./task-lease": "./src/task-lease.js",
77
82
  "./providers/github": "./src/providers/github.js",
78
83
  "./providers/google": "./src/providers/google.js"
79
84
  },
80
- "files": ["src", "docs", "examples", "README.md", "LICENSE", "SECURITY.md", "ROADMAP.md", "CONTRIBUTING.md"],
85
+ "files": ["src", "docs", "examples", "benchmarks", "README.md", "LICENSE", "SECURITY.md", "ROADMAP.md", "CONTRIBUTING.md"],
81
86
  "repository": { "type": "git", "url": "git+https://github.com/Null-Square/agent-authority.git" },
82
87
  "bugs": { "url": "https://github.com/Null-Square/agent-authority/issues" },
83
88
  "homepage": "https://github.com/Null-Square/agent-authority#readme"
package/src/cli.js CHANGED
@@ -6,7 +6,7 @@ import { createAgentToken, parseTtl } from './agent-auth.js';
6
6
  import { createRuntimeEnvironment } from './runtime-env.js';
7
7
  import { authorityHome, ensureAuthorityHome, loadConfig, saveConfig } from './storage.js';
8
8
 
9
- const VERSION = '0.3.0';
9
+ const VERSION = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
10
10
 
11
11
  function fail(message, code = 1) {
12
12
  console.error(`error: ${message}`);
@@ -144,7 +144,7 @@ async function connectionsCommand(args) {
144
144
  }
145
145
 
146
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');
147
+ if (!has(args, '--token-stdin')) throw new Error('GitHub local onboarding currently requires --token-stdin');
148
148
  const home = homeFrom(args);
149
149
  const env = createRuntimeEnvironment({ home });
150
150
  const token = await readStdinSecret();
@@ -184,7 +184,7 @@ async function disconnectGitHub(args) {
184
184
  const accountId = flag(args, '--account', 'default');
185
185
  const result = env.broker.disconnect({ principal_id: env.config.principal_id, service: 'github', account_id: accountId });
186
186
  if (!result) throw new Error(`GitHub account ${accountId} is not connected`);
187
- console.log(`✓ GitHub ${accountId} disconnected and local credential removed`);
187
+ console.log(`✓ GitHub ${result.account_id} disconnected and local credential removed`);
188
188
  }
189
189
 
190
190
  async function agentCommand(args) {
@@ -116,8 +116,19 @@ export class CredentialBroker {
116
116
  return connection;
117
117
  }
118
118
 
119
+ resolveConnection({ principal_id, service, account_id = 'default' }) {
120
+ const exact = this.connections.get({ principal_id, service, account_id });
121
+ if (exact) return exact;
122
+ if (account_id !== 'default') return null;
123
+
124
+ const candidates = this.connections.list(principal_id)
125
+ .filter((connection) => connection.service === service && connection.status === 'active');
126
+
127
+ return candidates.length === 1 ? candidates[0] : null;
128
+ }
129
+
119
130
  getConnection({ principal_id, service, account_id = 'default' }) {
120
- return this.connections.get({ principal_id, service, account_id });
131
+ return this.resolveConnection({ principal_id, service, account_id });
121
132
  }
122
133
 
123
134
  listConnections(principal_id) {
@@ -125,7 +136,7 @@ export class CredentialBroker {
125
136
  }
126
137
 
127
138
  resolveInternal({ principal_id, service, account_id = 'default' }) {
128
- const connection = this.connections.get({ principal_id, service, account_id });
139
+ const connection = this.resolveConnection({ principal_id, service, account_id });
129
140
  if (!connection || connection.status !== 'active') {
130
141
  const error = new Error(`no active ${service} connection for this principal`);
131
142
  error.code = 'connection_required';
@@ -139,10 +150,14 @@ export class CredentialBroker {
139
150
  }
140
151
 
141
152
  disconnect({ principal_id, service, account_id = 'default' }) {
142
- const current = this.connections.get({ principal_id, service, account_id });
153
+ const current = this.resolveConnection({ principal_id, service, account_id });
143
154
  if (!current) return null;
144
155
  this.secrets.delete(current.credential_ref);
145
- const connection = this.connections.disconnect({ principal_id, service, account_id });
156
+ const connection = this.connections.disconnect({
157
+ principal_id,
158
+ service,
159
+ account_id: current.account_id
160
+ });
146
161
  if (!connection) return null;
147
162
  const { credential_ref, ...safe } = connection;
148
163
  return safe;