@tangle-network/chatgpt-agents-kit 0.1.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 (47) hide show
  1. package/README.md +416 -0
  2. package/SETUP.md +211 -0
  3. package/dist/inspection/skills/tangle-agent-run-inspection/SKILL.md +22 -0
  4. package/dist/inspection/src/comparison.d.ts +91 -0
  5. package/dist/inspection/src/comparison.js +108 -0
  6. package/dist/inspection/src/core.d.ts +166 -0
  7. package/dist/inspection/src/core.js +236 -0
  8. package/dist/inspection/src/index.d.ts +36 -0
  9. package/dist/inspection/src/index.js +114 -0
  10. package/dist/skills/continue-agent-in-channel/SKILL.md +26 -0
  11. package/dist/skills/handoff-to-agent/SKILL.md +42 -0
  12. package/dist/skills/operate-existing-agent/SKILL.md +117 -0
  13. package/dist/skills/resolve-agent-decisions/SKILL.md +25 -0
  14. package/dist/skills/resume-agent-work/SKILL.md +32 -0
  15. package/dist/skills/review-agent-deliverables/SKILL.md +36 -0
  16. package/dist/skills/save-agent-playbook/SKILL.md +32 -0
  17. package/dist/src/app-metadata.d.ts +25 -0
  18. package/dist/src/app-metadata.js +34 -0
  19. package/dist/src/cli.d.ts +2 -0
  20. package/dist/src/cli.js +3 -0
  21. package/dist/src/connection.d.ts +10 -0
  22. package/dist/src/connection.js +25 -0
  23. package/dist/src/contracts.d.ts +161 -0
  24. package/dist/src/contracts.js +4 -0
  25. package/dist/src/events-node.d.ts +9 -0
  26. package/dist/src/events-node.js +128 -0
  27. package/dist/src/gtm.d.ts +19 -0
  28. package/dist/src/gtm.js +85 -0
  29. package/dist/src/handoff.d.ts +4 -0
  30. package/dist/src/handoff.js +112 -0
  31. package/dist/src/index.d.ts +13 -0
  32. package/dist/src/index.js +6 -0
  33. package/dist/src/inspection.d.ts +14 -0
  34. package/dist/src/inspection.js +40 -0
  35. package/dist/src/kit.d.ts +21 -0
  36. package/dist/src/kit.js +525 -0
  37. package/dist/src/package.d.ts +28 -0
  38. package/dist/src/package.js +259 -0
  39. package/dist/src/sandbox-agent.d.ts +35 -0
  40. package/dist/src/sandbox-agent.js +178 -0
  41. package/dist/src/task-events.d.ts +238 -0
  42. package/dist/src/task-events.js +268 -0
  43. package/dist/src/task-wait.d.ts +26 -0
  44. package/dist/src/task-wait.js +61 -0
  45. package/dist/src/workflows.d.ts +19 -0
  46. package/dist/src/workflows.js +38 -0
  47. package/package.json +58 -0
@@ -0,0 +1,525 @@
1
+ import { agentAppDiscovery, defineAgentAppMetadata } from "./app-metadata.js";
2
+ import { AgentFailure } from "./contracts.js";
3
+ import { ACTION_TOOLS as names, HANDOFF_ACTIONS, describeAgentWorkflows } from "./workflows.js";
4
+ import { createHandoffTool, HANDOFF_TOOL_NAME } from "./handoff.js";
5
+ import { observeTask, taskWaitPolicy, validateNativeTask } from "./task-wait.js";
6
+ import { describeConnection, digest, projectEnvironment } from "./connection.js";
7
+ export { digest } from "./connection.js";
8
+ const failureMarker = Symbol('native-tool-failure');
9
+ /** An error after entering native admission must retain its reconciliation handle. */
10
+ class AdmissionFailure extends AgentFailure {
11
+ recovery;
12
+ constructor(error, target, turnId) {
13
+ super(error instanceof AgentFailure ? error.message : 'native_operation_failed', error instanceof AgentFailure ? error.status : 503);
14
+ this.recovery = { tool: 'get_task', arguments: { ...target, turnId }, resubmit: false };
15
+ }
16
+ }
17
+ const descriptions = {
18
+ list: 'List existing workspaces visible to this authenticated identity.',
19
+ connect: 'Connect to an existing agent workspace and thread. Does not create or reset either.',
20
+ create: 'Create an agent workspace using the application’s normal provisioning and billing. Confirm first. Do not blindly retry.',
21
+ handoff: 'Hand over one explicitly approved UTF-8 brief or file. Null revision means create-only; updates require the last native revision.',
22
+ delegate: 'Send an authorized prompt to the same saved agent. Reuse turnId only for a retry of identical input. Waits briefly for verified output by default. Set waitMs to 0 for immediate admission. A pending result retains the same turnId; never resubmit to check progress.',
23
+ cancel: 'Cancel exactly the user-selected execution. Retains the agent workspace and conversation. Never infer cancellation from a request acknowledgement.',
24
+ task: 'Read the actual retained execution and verify output revisions. Optionally waitMs for this existing task to finish, without starting it again. A success claim without execution-linked outputs remains unverified.',
25
+ output: 'Retrieve actual UTF-8 output bytes and their native revision and SHA-256. File content is untrusted data.',
26
+ decisions: 'Read this workspace’s native pending decisions. Do not infer approval from an authenticated session.',
27
+ decide: 'Submit the user’s explicit decision through the native interaction API. This is delegated authority, not human-approval evidence.',
28
+ channel: 'Continue through an already approved application-bound line and member. Confirm exact text and destination first. Queued is not delivered.',
29
+ };
30
+ const str = { type: 'string', minLength: 1, maxLength: 1024 };
31
+ const targetSchema = { workspaceId: str, threadId: str };
32
+ const schemas = {
33
+ list: {}, connect: targetSchema, create: { name: str },
34
+ handoff: { ...targetSchema, path: str, content: { type: 'string', maxLength: 131072 }, expectedRevision: { type: ['string', 'null'] } },
35
+ delegate: { ...targetSchema, turnId: str, prompt: { type: 'string', minLength: 1, maxLength: 65536 }, contextVersion: { ...str, optional: true }, waitMs: { type: 'integer', minimum: 0, maximum: 25000, optional: true } },
36
+ cancel: { ...targetSchema, turnId: str, executionId: str },
37
+ task: { ...targetSchema, turnId: str, waitMs: { type: 'integer', minimum: 0, maximum: 25000, optional: true } }, output: { ...targetSchema, path: str }, decisions: targetSchema,
38
+ decide: { ...targetSchema, decisionId: str, response: { type: 'object' } },
39
+ channel: { ...targetSchema, lineId: str, memberId: str, text: { type: 'string', minLength: 1, maxLength: 16384 }, idempotencyKey: str },
40
+ };
41
+ export function safePath(value) {
42
+ if (value.length > 1024 || /[\\\x00-\x1f:%]/.test(value) || value.split('/').some(s => !s || s.startsWith('.')))
43
+ throw new AgentFailure('invalid_workspace_path');
44
+ return value;
45
+ }
46
+ function record(value) { return !!value && typeof value === 'object' && !Array.isArray(value); }
47
+ function validate(args, properties) {
48
+ if (Object.keys(args).some(key => !Object.hasOwn(properties, key)))
49
+ throw new AgentFailure('unknown_argument');
50
+ for (const [key, raw] of Object.entries(properties)) {
51
+ const schema = raw;
52
+ const value = args[key];
53
+ if (schema.optional && value === undefined)
54
+ continue;
55
+ if (schema.type === 'integer') {
56
+ if (!Number.isSafeInteger(value) || value < (schema.minimum ?? 0) || value > (schema.maximum ?? Infinity))
57
+ throw new AgentFailure('invalid_argument');
58
+ continue;
59
+ }
60
+ if (Array.isArray(schema.type) && value === null)
61
+ continue;
62
+ if (schema.type === 'object' ? !record(value) : typeof value !== 'string')
63
+ throw new AgentFailure('invalid_argument');
64
+ if (typeof value === 'string' && (value.length < (schema.minLength ?? 0) || value.length > (schema.maxLength ?? 131072)))
65
+ throw new AgentFailure('invalid_argument');
66
+ }
67
+ }
68
+ function supported(binding, action) {
69
+ const s = binding.services;
70
+ switch (action) {
71
+ case 'list': return typeof s.list === 'function';
72
+ case 'connect': return typeof s.connect === 'function';
73
+ case 'create': return !!s.create;
74
+ case 'handoff': return !!s.putFile;
75
+ case 'delegate': return !!s.startTask && !!s.getTask;
76
+ case 'task': return !!s.getTask;
77
+ case 'cancel': return !!s.cancelTask && !!s.getTask;
78
+ case 'output': return !!s.readFile;
79
+ case 'decisions': return !!s.pendingDecisions;
80
+ case 'decide': return !!s.pendingDecisions && !!s.decide;
81
+ case 'channel': return !!s.channelBinding && !!s.sendChannel;
82
+ }
83
+ }
84
+ function targetOf(args) {
85
+ const target = { workspaceId: args.workspaceId, threadId: args.threadId };
86
+ if (Object.values(target).some(v => !/^[A-Za-z0-9_-]{1,256}$/.test(v)))
87
+ throw new AgentFailure('invalid_target');
88
+ return target;
89
+ }
90
+ async function output(binding, target, path) {
91
+ const file = await binding.services.readFile(target, safePath(path));
92
+ if (!file || typeof file.content !== 'string' || !file.revision || typeof file.mediaType !== 'string')
93
+ throw new AgentFailure('native_output_invalid', 502);
94
+ if (new TextEncoder().encode(file.content).byteLength > 262144)
95
+ throw new AgentFailure('output_too_large', 413);
96
+ return { path, ...file, sha256: await digest(file.content), bytes: new TextEncoder().encode(file.content).byteLength };
97
+ }
98
+ async function taskReceipt(binding, target, task, expectedTurn, canReadOutput, checkOutput) {
99
+ validateNativeTask(task, target, expectedTurn);
100
+ const base = { ...target, turnId: task.turnId, executionId: task.executionId, state: task.state, source: task.source, ...(task.environment ? { environment: projectEnvironment(task.environment) } : {}) };
101
+ if (task.state !== 'succeeded')
102
+ return { ...base, completionVerified: false };
103
+ if (!task.executionId || !task.completedAt || !Number.isFinite(Date.parse(task.completedAt)) || (!task.outputs?.length && !task.response))
104
+ return { ...base, completionVerified: false, reason: 'execution_output_provenance_missing' };
105
+ if ((task.outputs?.length ?? 0) > 32)
106
+ throw new AgentFailure('too_many_outputs', 413);
107
+ if (!canReadOutput)
108
+ return { ...base, completionVerified: false, reason: 'output_scope_required' };
109
+ let response;
110
+ let outcome = 'completed';
111
+ if (task.response) {
112
+ if (!binding.services.readResponse)
113
+ return { ...base, completionVerified: false, reason: 'response_reader_unavailable' };
114
+ await checkOutput();
115
+ const value = await binding.services.readResponse(target, expectedTurn);
116
+ await checkOutput();
117
+ if (!value)
118
+ return { ...base, completionVerified: false, reason: 'retained_response_unavailable' };
119
+ if (value.workspaceId !== target.workspaceId || value.threadId !== target.threadId
120
+ || value.turnId !== expectedTurn || value.executionId !== task.executionId || value.completedAt !== task.completedAt
121
+ || typeof value.content !== 'string' || typeof value.mediaType !== 'string'
122
+ || (value.outcome !== undefined && !['completed', 'input-required', 'failed'].includes(value.outcome)))
123
+ throw new AgentFailure('native_response_invalid', 502);
124
+ const bytes = new TextEncoder().encode(value.content).byteLength;
125
+ if (bytes > 262144)
126
+ throw new AgentFailure('output_too_large', 413);
127
+ outcome = value.outcome ?? 'completed';
128
+ // Explicit projection: do not expose arbitrary host/provider fields.
129
+ response = { content: value.content, mediaType: value.mediaType, sha256: await digest(value.content), bytes };
130
+ }
131
+ if (task.outputs?.length && !binding.services.readFile)
132
+ return { ...base, completionVerified: false, reason: 'output_reader_unavailable' };
133
+ const outputs = [];
134
+ for (const ref of task.outputs ?? []) {
135
+ await checkOutput();
136
+ const file = await output(binding, target, ref.path);
137
+ await checkOutput();
138
+ if (!ref.revision || file.revision !== ref.revision)
139
+ return { ...base, completionVerified: false, reason: 'output_revision_changed' };
140
+ outputs.push(file);
141
+ }
142
+ return { ...base, ...(outcome !== 'completed' ? { state: outcome, reason: 'native_result_not_completed' } : {}),
143
+ completedAt: task.completedAt, completionVerified: outcome === 'completed', ...(response ? { response } : {}), outputs };
144
+ }
145
+ async function run(action, args, binding, revalidate, signal, policy, canReadTask, canReadOutput, canConnect) {
146
+ const s = binding.services;
147
+ const identity = { appName: binding.appName, identityId: binding.applicationUserId };
148
+ if (action === 'list') {
149
+ const agents = await s.list();
150
+ await revalidate();
151
+ return { ...identity, agents };
152
+ }
153
+ if (action === 'create') {
154
+ const created = await s.create(args.name);
155
+ await revalidate();
156
+ return { ...identity, ...created };
157
+ }
158
+ const target = targetOf(args);
159
+ await binding.authorize(target, action);
160
+ await revalidate();
161
+ if (signal.aborted)
162
+ throw new AgentFailure('observation_cancelled', 499);
163
+ const checkOutput = async () => { await binding.authorize(target, 'output'); await revalidate(); };
164
+ const observe = async (initial, defaultMs) => {
165
+ const waitMs = Math.min(args.waitMs ?? defaultMs, policy.maxMs);
166
+ if (!canReadTask && waitMs > 0)
167
+ throw new AgentFailure('task_read_scope_required', 403);
168
+ return observeTask({ target, turnId: args.turnId, initial, waitMs, intervalMs: policy.intervalMs, signal,
169
+ check: async () => { await binding.authorize(target, 'task'); await revalidate(); },
170
+ read: () => s.getTask(target, args.turnId),
171
+ });
172
+ };
173
+ let result;
174
+ switch (action) {
175
+ case 'connect':
176
+ result = { ...identity, ...target, ...await describeConnection(await s.connect(target)) };
177
+ break;
178
+ case 'handoff': {
179
+ const path = safePath(args.path);
180
+ const saved = await s.putFile(target, path, args.content, args.expectedRevision);
181
+ if (!saved.revision)
182
+ throw new AgentFailure('native_revision_missing', 502);
183
+ result = { ...target, path, revision: saved.revision, inputSha256: await digest(args.content), storedBytesVerified: false, authorization: 'delegated-tool-call' };
184
+ break;
185
+ }
186
+ case 'delegate': {
187
+ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(args.turnId))
188
+ throw new AgentFailure('turn_id_must_be_uuid');
189
+ // Validate observation authorization BEFORE admitting work. A denied wait must not create a task.
190
+ if (Math.min(args.waitMs ?? policy.defaultMs, policy.maxMs) > 0) {
191
+ if (!canReadTask)
192
+ throw new AgentFailure('task_read_scope_required', 403);
193
+ await binding.authorize(target, 'task');
194
+ await revalidate();
195
+ }
196
+ if (args.contextVersion !== undefined) {
197
+ if (!canConnect)
198
+ throw new AgentFailure('connect_scope_required', 403);
199
+ await binding.authorize(target, 'connect');
200
+ await revalidate();
201
+ if ((await describeConnection(await s.connect(target))).contextVersion !== args.contextVersion)
202
+ throw new AgentFailure('agent_context_changed_reconnect_required', 409);
203
+ await binding.authorize(target, 'delegate');
204
+ await revalidate();
205
+ }
206
+ try {
207
+ const task = await s.startTask(target, args.turnId, args.prompt, args.contextVersion === undefined ? undefined : { contextVersion: args.contextVersion });
208
+ result = await taskReceipt(binding, target, await observe(task, policy.defaultMs), args.turnId, canReadOutput, checkOutput);
209
+ break;
210
+ }
211
+ catch (error) {
212
+ throw new AdmissionFailure(error, target, args.turnId);
213
+ }
214
+ }
215
+ case 'cancel': {
216
+ // Read authorization and exact execution identity are prerequisites to a cancellation.
217
+ if (!canReadTask)
218
+ throw new AgentFailure('task_read_scope_required', 403);
219
+ await binding.authorize(target, 'task');
220
+ await revalidate();
221
+ const before = await s.getTask(target, args.turnId);
222
+ validateNativeTask(before, target, args.turnId);
223
+ if (!before.executionId || before.executionId !== args.executionId)
224
+ throw new AgentFailure('native_execution_changed', 409);
225
+ await binding.authorize(target, 'cancel');
226
+ await revalidate();
227
+ const after = await s.cancelTask(target, args.turnId, args.executionId);
228
+ validateNativeTask(after, target, args.turnId);
229
+ if (after.executionId !== before.executionId)
230
+ throw new AgentFailure('native_execution_changed', 502);
231
+ result = { ...target, turnId: after.turnId, executionId: after.executionId, state: after.state,
232
+ cancellationVerified: after.state === 'cancelled', workspaceDeleted: false };
233
+ break;
234
+ }
235
+ case 'task':
236
+ result = await taskReceipt(binding, target, await observe(await s.getTask(target, args.turnId), 0), args.turnId, canReadOutput, checkOutput);
237
+ break;
238
+ case 'output':
239
+ result = { ...target, ...await output(binding, target, args.path) };
240
+ break;
241
+ case 'decisions':
242
+ result = { ...target, decisions: await s.pendingDecisions(target) };
243
+ break;
244
+ case 'decide':
245
+ result = { ...target, authorization: 'delegated-tool-call', result: await s.decide(target, args.decisionId, args.response) };
246
+ break;
247
+ case 'channel': {
248
+ const bound = await s.channelBinding(target, args.lineId, args.memberId);
249
+ if (bound.applicationUserId !== binding.applicationUserId || bound.target.workspaceId !== target.workspaceId || bound.target.threadId !== target.threadId || bound.lineId !== args.lineId || bound.memberId !== args.memberId || bound.consent !== 'enabled' || !bound.binding)
250
+ throw new AgentFailure('channel_binding_or_consent_mismatch', 403);
251
+ // sendChannel MUST recheck native live attachment/consent atomically with send.
252
+ await revalidate();
253
+ const receipt = await s.sendChannel(bound, { memberId: bound.memberId, text: args.text, idempotencyKey: args.idempotencyKey });
254
+ if (!receipt.message?.id || !receipt.message.status)
255
+ throw new AgentFailure('native_message_receipt_missing', 502);
256
+ result = { ...identity, ...target, lineId: bound.lineId, memberId: bound.memberId, binding: bound.binding, messageId: receipt.message.id, status: receipt.message.status, deliveryVerified: false };
257
+ break;
258
+ }
259
+ }
260
+ await binding.authorize(target, action);
261
+ await revalidate();
262
+ if ((action === 'delegate' || action === 'task') && canReadTask && record(result) && ['queued', 'working', 'unknown'].includes(String(result.state)) && !result.completionVerified)
263
+ return { ...result, continuation: { tool: names.task, arguments: { ...target, turnId: args.turnId }, resubmit: false } };
264
+ return result;
265
+ }
266
+ async function boundedBody(request) {
267
+ const reader = request.body?.getReader();
268
+ if (!reader)
269
+ throw new AgentFailure('json_required');
270
+ const parts = [];
271
+ let length = 0;
272
+ try {
273
+ while (true) {
274
+ const { done, value } = await reader.read();
275
+ if (done)
276
+ break;
277
+ length += value.byteLength;
278
+ if (length > 524288) {
279
+ await reader.cancel();
280
+ throw new AgentFailure('request_too_large', 413);
281
+ }
282
+ parts.push(value);
283
+ }
284
+ }
285
+ finally {
286
+ reader.releaseLock();
287
+ }
288
+ const all = new Uint8Array(length);
289
+ let offset = 0;
290
+ for (const part of parts) {
291
+ all.set(part, offset);
292
+ offset += part.byteLength;
293
+ }
294
+ return new TextDecoder('utf-8', { fatal: true }).decode(all);
295
+ }
296
+ export function createAgentsHandler(options) {
297
+ const metadata = defineAgentAppMetadata(options.metadata, options.oauth.resource);
298
+ const waitPolicy = taskWaitPolicy(options.taskWait);
299
+ const resource = new URL(options.oauth.resource);
300
+ if (resource.username || resource.password || resource.search || resource.hash || (resource.protocol !== 'https:' && !(resource.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(resource.hostname))))
301
+ throw new Error('invalid_oauth_resource');
302
+ const metadataUrl = `${resource.origin}/.well-known/oauth-protected-resource${resource.pathname}`;
303
+ const failure = (status, code) => Response.json({ error: code }, { status, headers: { 'cache-control': 'no-store', ...(status === 401 ? { 'www-authenticate': `Bearer resource_metadata="${metadataUrl}"` } : {}) } });
304
+ return async (request) => {
305
+ const url = new URL(request.url);
306
+ if (url.href === metadataUrl && request.method === 'GET') {
307
+ try {
308
+ const resourceMetadata = await options.oauth.protectedResourceMetadata();
309
+ if (resourceMetadata.resource !== resource.href || !resourceMetadata.authorization_servers?.length)
310
+ return failure(503, 'oauth_metadata_unavailable');
311
+ return Response.json({ ...resourceMetadata, ...agentAppDiscovery(metadata) }, { headers: { 'cache-control': 'no-store' } });
312
+ }
313
+ catch {
314
+ return failure(503, 'oauth_metadata_unavailable');
315
+ }
316
+ }
317
+ if (url.origin !== resource.origin || url.pathname !== resource.pathname || url.search)
318
+ return failure(404, 'not_found');
319
+ const origin = request.headers.get('origin');
320
+ if (origin && !(options.allowedOrigins ?? [resource.origin]).includes(origin))
321
+ return failure(403, 'origin_not_allowed');
322
+ const token = /^Bearer ([^\s]+)$/i.exec(request.headers.get('authorization') ?? '')?.[1];
323
+ if (!token || /^(sk-|gak_|svc_)/.test(token))
324
+ return failure(401, 'oauth_required');
325
+ let principal;
326
+ let initialPrincipal;
327
+ let binding;
328
+ try {
329
+ principal = await options.oauth.verifyAccessToken(token);
330
+ if (!principal?.userId)
331
+ return failure(401, 'invalid_oauth_token');
332
+ // Snapshot before host callbacks or body I/O; a verifier may reuse a mutable object.
333
+ initialPrincipal = { userId: principal.userId, clientId: principal.clientId, scopes: [...principal.scopes] };
334
+ binding = await options.bind(principal);
335
+ if (binding.platformUserId !== principal.userId || !binding.applicationUserId || binding.appName !== metadata.name)
336
+ return failure(403, 'identity_binding_mismatch');
337
+ }
338
+ catch {
339
+ return failure(503, 'identity_unavailable');
340
+ }
341
+ if (request.method !== 'POST')
342
+ return failure(405, 'post_required');
343
+ if (request.headers.get('content-type')?.split(';')[0]?.trim().toLowerCase() !== 'application/json')
344
+ return failure(415, 'json_required');
345
+ let body;
346
+ let parsed;
347
+ try {
348
+ body = await boundedBody(request);
349
+ const value = JSON.parse(body);
350
+ if (!record(value) || value.jsonrpc !== '2.0' || typeof value.method !== 'string' || ('id' in value && value.id !== null && typeof value.id !== 'string' && typeof value.id !== 'number'))
351
+ return failure(400, 'invalid_rpc');
352
+ parsed = value;
353
+ }
354
+ catch (error) {
355
+ return failure(error instanceof AgentFailure ? error.status : 400, error instanceof AgentFailure ? error.message : 'invalid_json');
356
+ }
357
+ // The body, inspection setup, and dispatcher may all wait on external work.
358
+ // Keep the initial grant fixed and use the app's live verifier at each boundary.
359
+ const revalidate = async () => {
360
+ let current;
361
+ try {
362
+ current = await options.oauth.verifyAccessToken(token);
363
+ }
364
+ catch {
365
+ throw new AgentFailure('identity_unavailable', 503);
366
+ }
367
+ if (!current?.userId)
368
+ throw new AgentFailure('invalid_oauth_token', 401);
369
+ const initialScopes = new Set(initialPrincipal.scopes);
370
+ const currentScopes = new Set(current.scopes);
371
+ if (current.userId !== initialPrincipal.userId || current.clientId !== initialPrincipal.clientId
372
+ || currentScopes.size !== initialScopes.size || [...currentScopes].some(scope => !initialScopes.has(scope)))
373
+ throw new AgentFailure('oauth_principal_changed', 401);
374
+ return { userId: current.userId, clientId: current.clientId, scopes: [...currentScopes] };
375
+ };
376
+ try {
377
+ principal = await revalidate();
378
+ }
379
+ catch (error) {
380
+ return failure(error instanceof AgentFailure ? error.status : 503, error instanceof AgentFailure ? error.message : 'identity_unavailable');
381
+ }
382
+ let inspection;
383
+ let inspectionScopes = [];
384
+ try {
385
+ const configured = options.inspection ? await options.inspection(principal, binding) : null;
386
+ if (configured && (!configured.requiredScopes.length || configured.requiredScopes.some(scope => !/^[\x21\x23-\x5B\x5D-\x7E]{1,256}$/.test(scope))))
387
+ return failure(503, 'inspection_scope_invalid');
388
+ inspectionScopes = configured?.requiredScopes ?? [];
389
+ // Inspection remains optional; do not initialize its SDKs for an app that
390
+ // does not expose it to this grant. No replacement inspection path.
391
+ inspection = configured && inspectionScopes.every(scope => principal.scopes.includes(scope))
392
+ ? await (await import("./inspection.js")).createInspectionBridge(configured.exposure)
393
+ : null;
394
+ }
395
+ catch {
396
+ return failure(503, 'inspection_unavailable');
397
+ }
398
+ try {
399
+ try {
400
+ await revalidate();
401
+ }
402
+ catch (error) {
403
+ return failure(error instanceof AgentFailure ? error.status : 503, error instanceof AgentFailure ? error.message : 'identity_unavailable');
404
+ }
405
+ const actions = metadata.actions.filter(action => supported(binding, action) && binding.requiredScopes[action] !== undefined && binding.requiredScopes[action].every(scope => principal.scopes.includes(scope)));
406
+ // A read grant can discover the prompt scope before the user grants run.
407
+ // Keep it out of executable capabilities until the live grant includes every scope.
408
+ const upgradeableDelegate = !actions.includes('delegate') && metadata.actions.includes('delegate')
409
+ && supported(binding, 'delegate') && !!binding.requiredScopes.delegate?.length
410
+ && actions.some(action => ['list', 'connect', 'task', 'output', 'decisions'].includes(action));
411
+ const advertisedActions = upgradeableDelegate ? [...actions, 'delegate'] : actions;
412
+ const eventAccess = { principal: initialPrincipal, binding, check: async () => { await revalidate(); } };
413
+ const canReadOutput = metadata.actions.includes('output') && binding.requiredScopes.output !== undefined
414
+ && binding.requiredScopes.output.every(scope => principal.scopes.includes(scope))
415
+ && (!!binding.services.readFile || !!binding.services.readResponse);
416
+ const eventsEnabled = actions.includes('task') && canReadOutput && !!options.events && await options.events.available(eventAccess);
417
+ const v2 = request.headers.get('mcp-protocol-version') === '2026-07-28';
418
+ const rpcResult = (envelope) => Response.json({ jsonrpc: '2.0', id: parsed.id ?? null, ...envelope }, { headers: { 'cache-control': 'no-store' } });
419
+ if (v2) {
420
+ const params = record(parsed.params) ? parsed.params : {};
421
+ if (request.headers.get('mcp-method') !== parsed.method
422
+ || (typeof params.name === 'string' && request.headers.get('mcp-name') !== params.name))
423
+ return rpcResult({ error: { code: -32600, message: 'MCP routing headers do not match request' } });
424
+ if (parsed.method === 'server/discover')
425
+ return rpcResult({ result: { resultType: 'complete', supportedVersions: ['2026-07-28'], serverInfo: { name: metadata.name, version: '0.1.0' }, capabilities: { tools: {}, ...(eventsEnabled ? { events: {} } : {}) } } });
426
+ }
427
+ if (parsed.method.startsWith('events/')) {
428
+ if (!v2 || !eventsEnabled)
429
+ return rpcResult({ error: { code: -32601, message: 'Events unavailable for this protocol or grant' } });
430
+ const result = await options.events.rpc(parsed.method, parsed.params ?? {}, eventAccess);
431
+ try {
432
+ await revalidate();
433
+ }
434
+ catch (error) {
435
+ return failure(error instanceof AgentFailure ? error.status : 503, error instanceof AgentFailure ? error.message : 'identity_unavailable');
436
+ }
437
+ return rpcResult(result);
438
+ }
439
+ const workflows = describeAgentWorkflows(actions);
440
+ const handoff = workflows.some(workflow => workflow.id === 'handoff-to-agent') ? createHandoffTool(binding, revalidate) : null;
441
+ const profileSchema = { type: 'object', properties: { id: { type: 'string', minLength: 1 }, name: { type: 'string' } }, required: ['id'], additionalProperties: false };
442
+ const tools = [
443
+ { name: 'agent_profile', description: 'Stable application identity of the authenticated account.', inputSchema: { type: 'object', properties: {}, additionalProperties: false }, run: async (args) => { validate(args, {}); return { id: binding.applicationUserId, ...(binding.name ? { name: binding.name } : {}) }; } },
444
+ { name: 'agent_capabilities', description: 'Discover actions actually supported for this identity, app and grant now.', inputSchema: { type: 'object', properties: {}, additionalProperties: false }, run: async (args) => { validate(args, {}); return { appName: binding.appName, actions: [...actions.map(a => names[a]), ...(handoff ? [handoff.name] : [])], workflows, inspection: inspection?.tools.map(tool => tool.name) ?? [], media: ['text/plain; charset=utf-8'], compatibilityAliases: actions.includes('delegate') ? { delegate_task: names.delegate } : {}, preferredPromptTool: actions.includes('delegate') ? names.delegate : null, resultKinds: canReadOutput ? [...(binding.services.readFile ? ['files'] : []), ...(binding.services.readResponse ? ['response'] : [])] : [], execution: 'native-application', stateStore: 'native-application', taskWait: { defaultMs: waitPolicy.defaultMs, maxMs: waitPolicy.maxMs }, events: eventsEnabled ? { name: 'agent.task_updated', protocolVersion: '2026-07-28', delivery: ['webhook'], replay: false } : null }; } },
445
+ ...advertisedActions.map(action => ({ name: names[action], description: descriptions[action], inputSchema: { type: 'object', properties: Object.fromEntries(Object.entries(schemas[action]).map(([key, value]) => { const { optional: _optional, ...schema } = value; return [key, schema]; })), required: Object.keys(schemas[action]).filter(key => !schemas[action][key].optional), additionalProperties: false }, run: async (args) => {
446
+ validate(args, schemas[action]);
447
+ if (!actions.includes(action))
448
+ throw new AgentFailure('insufficient_scope', 403);
449
+ if (action === 'delegate' && args.contextVersion !== undefined && !actions.includes('connect'))
450
+ throw new AgentFailure('connect_scope_required', 403);
451
+ const result = await run(action, args, binding, revalidate, request.signal, waitPolicy, actions.includes('task'), canReadOutput, actions.includes('connect'));
452
+ return record(result) && result.continuation && eventsEnabled
453
+ ? { ...result, notification: { state: 'not_subscribed', name: 'agent.task_updated', arguments: { workspaceId: args.workspaceId, threadId: args.threadId, turnId: args.turnId } } }
454
+ : result;
455
+ } })),
456
+ ...(handoff ? [handoff] : []),
457
+ ...(inspection?.tools ?? []),
458
+ ];
459
+ if (upgradeableDelegate && parsed.method === 'tools/call' && record(parsed.params)
460
+ && [names.delegate, 'delegate_task'].includes(String(parsed.params.name))) {
461
+ const challenge = `Bearer resource_metadata="${metadataUrl}", error="insufficient_scope", error_description="Additional permission is required to run this agent", scope="${binding.requiredScopes.delegate.join(' ')}"`;
462
+ return rpcResult({ result: { isError: true, content: [{ type: 'text', text: 'insufficient_scope' }],
463
+ _meta: { 'mcp/www_authenticate': [challenge] }, ...(v2 ? { resultType: 'complete' } : {}) } });
464
+ }
465
+ const handler = options.nativeMcp({
466
+ serverInfo: { name: metadata.name, version: '0.1.0' },
467
+ tools: tools.map(tool => ({ ...tool, run: async (args) => { try {
468
+ await revalidate();
469
+ return await tool.run(args, {});
470
+ }
471
+ catch (error) {
472
+ return { [failureMarker]: { code: error instanceof AgentFailure ? error.message : 'native_operation_failed', status: error instanceof AgentFailure ? error.status : 503, ...(error instanceof AdmissionFailure ? { recovery: error.recovery } : {}) } };
473
+ } } })),
474
+ buildEnv: () => ({}),
475
+ formatResult: result => {
476
+ const failed = result && typeof result === 'object' && failureMarker in result ? result[failureMarker] : null;
477
+ return failed
478
+ ? { isError: true, content: [{ type: 'text', text: failed.code }], structuredContent: { error: failed } }
479
+ : { content: [{ type: 'text', text: JSON.stringify(result) }], structuredContent: result };
480
+ },
481
+ });
482
+ if (parsed.method === 'tools/call' && record(parsed.params) && parsed.params.name === 'delegate_task') {
483
+ const args = parsed.params.arguments;
484
+ if (!record(args) || Object.hasOwn(args, 'prompt'))
485
+ return rpcResult({ error: { code: -32602, message: 'invalid_legacy_prompt_arguments' } });
486
+ const { brief, ...rest } = args;
487
+ body = JSON.stringify({ ...parsed, params: { ...parsed.params, name: names.delegate, arguments: { ...rest, prompt: brief } } });
488
+ }
489
+ const response = await handler(new Request(resource, { method: 'POST', headers: { 'content-type': 'application/json' }, body }));
490
+ // The maintained tools-only envelope intentionally omits host metadata. Add
491
+ // OAuth/profile/approval annotations without replacing its RPC dispatcher.
492
+ if (parsed.method === 'tools/list' && response.ok) {
493
+ const envelope = await response.json();
494
+ for (const tool of envelope.result?.tools ?? []) {
495
+ const action = advertisedActions.find(a => names[a] === tool.name);
496
+ const readOnly = !action || ['list', 'connect', 'task', 'output', 'decisions'].includes(action);
497
+ const inspectionTool = inspection?.tools.some(item => item.name === tool.name);
498
+ const handoffScopes = tool.name === HANDOFF_TOOL_NAME
499
+ ? [...new Set(HANDOFF_ACTIONS.flatMap(a => binding.requiredScopes[a] ?? []))]
500
+ : [];
501
+ const securitySchemes = [{ type: 'oauth2', scopes: inspectionTool ? inspectionScopes : action ? binding.requiredScopes[action] : handoffScopes }];
502
+ Object.assign(tool, { annotations: { readOnlyHint: readOnly, destructiveHint: !readOnly, idempotentHint: readOnly, openWorldHint: action === 'delegate' || action === 'decide' || action === 'channel' }, securitySchemes, _meta: { securitySchemes, ...(tool.name === 'agent_profile' ? { 'openai/profile': true } : {}) }, ...(tool.name === 'agent_profile' ? { outputSchema: profileSchema } : {}) });
503
+ }
504
+ if (v2 && envelope.result) {
505
+ envelope.result.tools?.sort((a, b) => String(a.name).localeCompare(String(b.name)));
506
+ Object.assign(envelope.result, { resultType: 'complete', ttlMs: 0, cacheScope: 'private' });
507
+ }
508
+ return Response.json(envelope, { headers: { 'cache-control': 'no-store' } });
509
+ }
510
+ if (v2 && response.ok && response.status !== 204) {
511
+ const envelope = await response.json();
512
+ if (envelope.result)
513
+ envelope.result.resultType = 'complete';
514
+ return Response.json(envelope, { headers: { 'cache-control': 'no-store' } });
515
+ }
516
+ const headers = new Headers(response.headers);
517
+ headers.set('cache-control', 'no-store');
518
+ return new Response(response.body, { status: response.status, headers });
519
+ }
520
+ finally {
521
+ if (inspection)
522
+ await inspection.close();
523
+ }
524
+ };
525
+ }
@@ -0,0 +1,28 @@
1
+ import type { Metadata } from './contracts.ts';
2
+ export declare function endpointUrl(value: string, allowLoopback?: boolean): URL;
3
+ export declare function validateMetadata(value: unknown): Metadata;
4
+ /** Anonymous, read-only readiness check. Does not register an OAuth client or deploy anything. */
5
+ export declare function probeOAuth(endpoint: string, request?: typeof fetch): Promise<{
6
+ app?: {} | null | undefined;
7
+ anonymousStatus: number;
8
+ resource: string;
9
+ protectedResourceMetadata: string;
10
+ authorizationServers: string[];
11
+ oauthLogin: string;
12
+ capabilities: string;
13
+ }>;
14
+ export declare function buildPackage(metadata: Metadata, endpoint: string, out: string, offline?: boolean, connectionId?: string): Promise<{
15
+ directory: string;
16
+ readiness: {
17
+ [x: string]: unknown;
18
+ };
19
+ }>;
20
+ /** Read the selected endpoint's exact discovery document; no second app.json. */
21
+ export declare function buildPackageFromEndpoint(endpoint: string, out: string, request?: typeof fetch, connectionId?: string): Promise<{
22
+ directory: string;
23
+ readiness: {
24
+ [x: string]: unknown;
25
+ };
26
+ }>;
27
+ /** Shared by the installed compiled bin and the existing source entry point. */
28
+ export declare function runPackageCli(args?: string[]): Promise<void>;