@margay/ccl-core 1.5.0-alpha.4 → 1.5.1-alpha.3

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 (65) hide show
  1. package/dist/cli.js +108954 -103254
  2. package/dist/sdk/background.d.ts +48 -0
  3. package/dist/sdk/background.js +87 -0
  4. package/dist/sdk/backgroundRunTypes.d.ts +101 -0
  5. package/dist/sdk/backgroundRunTypes.js +1 -0
  6. package/dist/sdk/cancellation.d.ts +15 -0
  7. package/dist/sdk/cancellation.js +18 -0
  8. package/dist/sdk/commandAdapter.d.ts +77 -0
  9. package/dist/sdk/commandAdapter.js +180 -0
  10. package/dist/sdk/commandProcess.d.ts +27 -0
  11. package/dist/sdk/commandProcess.js +130 -0
  12. package/dist/sdk/executionGuidance.d.ts +7 -0
  13. package/dist/sdk/executionGuidance.js +37 -0
  14. package/dist/sdk/index.d.ts +14 -0
  15. package/dist/sdk/index.js +9 -0
  16. package/dist/sdk/mcpServer.d.ts +9 -5
  17. package/dist/sdk/mcpServer.js +51 -9
  18. package/dist/sdk/operations/command.d.ts +24 -0
  19. package/dist/sdk/operations/command.js +48 -0
  20. package/dist/sdk/operations/host.d.ts +3 -0
  21. package/dist/sdk/operations/host.js +376 -0
  22. package/dist/sdk/operations/json.d.ts +6 -0
  23. package/dist/sdk/operations/json.js +24 -0
  24. package/dist/sdk/operations/reference.d.ts +15 -0
  25. package/dist/sdk/operations/reference.js +19 -0
  26. package/dist/sdk/operations/store.d.ts +26 -0
  27. package/dist/sdk/operations/store.js +300 -0
  28. package/dist/sdk/operations/types.d.ts +171 -0
  29. package/dist/sdk/operations/types.js +8 -0
  30. package/dist/sdk/pendingApprovalIdentity.d.ts +35 -0
  31. package/dist/sdk/pendingApprovalIdentity.js +27 -0
  32. package/dist/sdk/permissionDelegation.d.ts +19 -0
  33. package/dist/sdk/permissionDelegation.js +11 -0
  34. package/dist/sdk/query.d.ts +3 -1
  35. package/dist/sdk/query.js +142 -30
  36. package/dist/sdk/queryHandle.js +28 -8
  37. package/dist/sdk/queryLifecycle.d.ts +19 -0
  38. package/dist/sdk/queryLifecycle.js +93 -0
  39. package/dist/sdk/queryLifecycleTypes.d.ts +160 -0
  40. package/dist/sdk/queryLifecycleTypes.js +47 -0
  41. package/dist/sdk/scheduledOccurrenceTypes.d.ts +103 -0
  42. package/dist/sdk/scheduledOccurrenceTypes.js +12 -0
  43. package/dist/sdk/types.d.ts +21 -3
  44. package/docs/background-runs.md +229 -0
  45. package/docs/command-tool-recovery.md +62 -0
  46. package/docs/dev-quickstart.md +29 -7
  47. package/docs/error-recovery-usage.md +18 -0
  48. package/docs/hook-input-provenance.md +77 -0
  49. package/docs/index.md +2 -0
  50. package/docs/mcp-tool-recovery.md +82 -0
  51. package/docs/releases/1.5.0-alpha.5.md +26 -0
  52. package/docs/releases/1.5.0-alpha.6.md +15 -0
  53. package/docs/releases/1.5.1-alpha.1.md +11 -0
  54. package/docs/releases/1.5.1-alpha.2.md +17 -0
  55. package/docs/releases/1.5.1-alpha.3.md +23 -0
  56. package/docs/releases/1.5.1-alpha.md +22 -0
  57. package/docs/runtime-resources.md +6 -3
  58. package/examples/background-runs/agents/background-reviewer.md +6 -0
  59. package/examples/background-runs/agents/background-worker.md +6 -0
  60. package/examples/background-runs/worker-reviewer.mjs +42 -0
  61. package/examples/command-recovery/durable-server.mjs +44 -0
  62. package/examples/command-recovery/receipt-command.mjs +40 -0
  63. package/package.json +17 -2
  64. package/sdk/API.md +206 -1
  65. package/sdk/README.md +6 -0
@@ -0,0 +1,300 @@
1
+ import { createRequire } from 'node:module';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { closeSync, existsSync, fsyncSync, lstatSync, mkdirSync, openSync } from 'node:fs';
4
+ import { dirname, join, resolve } from 'node:path';
5
+ import { OperationError } from './types.js';
6
+ import { operationClone, operationDigest, operationFreeze, operationJson } from './json.js';
7
+ let Sqlite;
8
+ function database(path, options) {
9
+ Sqlite ??= createRequire(import.meta.url)('node:sqlite').DatabaseSync;
10
+ return new Sqlite(path, options);
11
+ }
12
+ function durableDirectory(path) {
13
+ if (existsSync(path))
14
+ return;
15
+ const parent = dirname(path);
16
+ durableDirectory(parent);
17
+ try {
18
+ mkdirSync(path, { mode: 0o700 });
19
+ }
20
+ catch (e) {
21
+ if (e.code !== 'EEXIST')
22
+ throw e;
23
+ }
24
+ syncDirectory(path);
25
+ syncDirectory(parent);
26
+ }
27
+ function syncDirectory(path) {
28
+ const fd = openSync(path, 'r');
29
+ try {
30
+ fsyncSync(fd);
31
+ }
32
+ finally {
33
+ closeSync(fd);
34
+ }
35
+ }
36
+ function sqlError(error) {
37
+ if (error instanceof OperationError)
38
+ throw error;
39
+ // SQLite may return an extended code (e.g. SQLITE_READONLY_ROLLBACK=776).
40
+ // Its low byte is the primary code used by these recovery decisions.
41
+ const rawCode = error.errcode;
42
+ const code = typeof rawCode === 'number' ? rawCode & 0xff : undefined;
43
+ if (code === 5 || code === 6)
44
+ throw new OperationError('busy', 'Operation storage is busy; query the original operation before trying again');
45
+ if (code === 8 || code === 14)
46
+ throw new OperationError('recovery_required', 'Operation storage requires writable recovery under its execution lock');
47
+ throw new OperationError('history_unavailable', 'Operation storage is unavailable or corrupt; this does not establish whether an earlier operation was dispatched');
48
+ }
49
+ function privateFile(path) { const fd = openSync(path, 'wx', 0o600); closeSync(fd); }
50
+ /** One authoritative transaction journal per operation; the separate database is only an OS lock. */
51
+ export class OperationStore {
52
+ storeDir;
53
+ scope;
54
+ root;
55
+ constructor(storeDir, scope) {
56
+ this.storeDir = storeDir;
57
+ this.scope = scope;
58
+ if (!scope)
59
+ throw new Error('Operation scope must be nonempty');
60
+ this.root = join(resolve(storeDir), operationDigest(scope));
61
+ }
62
+ idFor(requestKey) {
63
+ if (!requestKey)
64
+ throw new Error('requestKey must be nonempty');
65
+ return `op_${operationDigest([this.scope, requestKey])}`;
66
+ }
67
+ directory(operationId) {
68
+ if (!/^op_[a-f0-9]{64}$/.test(operationId))
69
+ throw new OperationError('not_found', 'Invalid operation identity');
70
+ const dir = join(this.root, operationId);
71
+ if (existsSync(dir) && (!lstatSync(dir).isDirectory() || lstatSync(dir).isSymbolicLink()))
72
+ throw new OperationError('history_unavailable', 'Operation directory is not a private directory');
73
+ return dir;
74
+ }
75
+ open(operationId, writable = false) {
76
+ const dir = this.directory(operationId), path = join(dir, 'journal.sqlite');
77
+ if (!existsSync(dir))
78
+ throw new OperationError('not_found', 'No operation history found; this does not prove an earlier external action never ran');
79
+ if (!existsSync(path) || !lstatSync(path).isFile() || lstatSync(path).isSymbolicLink())
80
+ throw new OperationError('history_unavailable', 'Operation journal is missing or invalid');
81
+ let db;
82
+ try {
83
+ db = database(path, { readOnly: !writable, timeout: 0, allowExtension: false });
84
+ if (Number(db.prepare('PRAGMA user_version').get()?.user_version) !== 1)
85
+ throw new OperationError('unsupported_version', 'Unknown operation schema; refusing to replace history');
86
+ if (writable)
87
+ db.exec('PRAGMA synchronous=FULL');
88
+ return db;
89
+ }
90
+ catch (e) {
91
+ db?.close();
92
+ return sqlError(e);
93
+ }
94
+ }
95
+ create(initial) {
96
+ const dir = this.directory(initial.operationId);
97
+ durableDirectory(this.root);
98
+ try {
99
+ mkdirSync(dir, { mode: 0o700 });
100
+ }
101
+ catch (error) {
102
+ if (error.code !== 'EEXIST')
103
+ return sqlError(error);
104
+ const existing = this.read(initial.operationId);
105
+ if (existing.requestKey !== initial.requestKey || existing.inputDigest !== initial.inputDigest || existing.adapterRef !== initial.adapterRef || operationDigest(existing.binding) !== operationDigest(initial.binding))
106
+ throw new OperationError('conflict', 'requestKey is already bound to different input or adapter');
107
+ return existing;
108
+ }
109
+ let db;
110
+ try {
111
+ privateFile(join(dir, 'executor.sqlite'));
112
+ const guard = database(join(dir, 'executor.sqlite'), { timeout: 0, allowExtension: false });
113
+ try {
114
+ guard.exec('PRAGMA journal_mode=DELETE; PRAGMA synchronous=FULL; CREATE TABLE mutex (value INTEGER)');
115
+ }
116
+ finally {
117
+ guard.close();
118
+ }
119
+ privateFile(join(dir, 'journal.sqlite'));
120
+ db = database(join(dir, 'journal.sqlite'), { timeout: 0, allowExtension: false });
121
+ db.exec(`PRAGMA journal_mode=DELETE; PRAGMA synchronous=FULL;
122
+ BEGIN IMMEDIATE;
123
+ CREATE TABLE events (sequence INTEGER PRIMARY KEY, body TEXT NOT NULL, checksum TEXT NOT NULL);
124
+ CREATE TABLE results (ref TEXT PRIMARY KEY, body TEXT NOT NULL, checksum TEXT NOT NULL);
125
+ PRAGMA user_version=1;`);
126
+ const state = { ...operationClone(initial), schemaVersion: 1, revision: 1, generation: randomUUID() };
127
+ this.append(db, state, 'accepted', '');
128
+ db.exec('COMMIT');
129
+ syncDirectory(dir);
130
+ syncDirectory(this.root);
131
+ return this.view(db, state);
132
+ }
133
+ catch (e) {
134
+ return sqlError(e);
135
+ }
136
+ finally {
137
+ db?.close();
138
+ }
139
+ }
140
+ append(db, state, event, previousHash) {
141
+ const frame = { schemaVersion: 1, operationId: state.operationId, sequence: state.revision, previousHash, event, state };
142
+ const body = operationJson(frame), checksum = operationDigest(frame);
143
+ db.prepare('INSERT INTO events(sequence,body,checksum) VALUES(?,?,?)').run(state.revision, body, checksum);
144
+ }
145
+ frames(db, operationId) {
146
+ const rows = db.prepare('SELECT sequence,body,checksum FROM events ORDER BY sequence').all();
147
+ if (rows.length === 0)
148
+ throw new OperationError('history_unavailable', 'Operation has no committed acceptance record');
149
+ let previousHash = '', sequence = 0;
150
+ return rows.map(row => {
151
+ const frame = JSON.parse(String(row.body));
152
+ if (frame.schemaVersion !== 1 || frame.operationId !== operationId || frame.state?.scope !== this.scope || frame.state?.operationId !== operationId || frame.state.schemaVersion !== 1 || frame.state.revision !== frame.sequence || frame.sequence !== ++sequence || frame.sequence !== row.sequence || frame.previousHash !== previousHash || operationDigest(frame) !== row.checksum)
153
+ throw new OperationError('history_unavailable', 'Operation event chain is corrupt; refusing recovery');
154
+ previousHash = String(row.checksum);
155
+ return { ...frame, checksum: previousHash };
156
+ });
157
+ }
158
+ result(db, ref) {
159
+ if (!ref)
160
+ return;
161
+ const row = db.prepare('SELECT body,checksum FROM results WHERE ref=?').get(ref);
162
+ if (!row)
163
+ throw new OperationError('history_unavailable', 'Operation result is missing');
164
+ const value = JSON.parse(String(row.body));
165
+ if (operationDigest(value) !== row.checksum)
166
+ throw new OperationError('history_unavailable', 'Operation result is corrupt');
167
+ return value;
168
+ }
169
+ view(db, state) {
170
+ const allowedActions = ['get', 'events'];
171
+ if (state.capabilities.query && state.effect !== 'confirmed')
172
+ allowedActions.push('reconcile');
173
+ if (state.effect === 'not_dispatched' && ['accepted', 'waiting_permission', 'failed', 'interrupted'].includes(state.execution) && state.cancellation === 'none')
174
+ allowedActions.push('execute');
175
+ if (state.effect === 'confirmed')
176
+ allowedActions.push('accept_verified_result');
177
+ else if (state.cancellation === 'none' && state.attempts.some(x => x.kind !== 'query')) {
178
+ const contract = state.capabilities.idempotency;
179
+ const replayable = state.capabilities.replay === 'read_only' || (state.capabilities.replay === 'idempotent'
180
+ && contract && contract.scope && Number.isFinite(contract.retentionMs) && contract.retentionMs > 0
181
+ && Date.now() - state.createdAt < contract.retentionMs);
182
+ if (replayable)
183
+ allowedActions.push('retry');
184
+ }
185
+ if (!['succeeded', 'cancelled'].includes(state.execution))
186
+ allowedActions.push('cancel');
187
+ return operationFreeze({ ...state, allowedActions, ...(state.resultRef ? { result: this.result(db, state.resultRef) } : {}), ...(state.receiptRef ? { receipt: this.result(db, state.receiptRef) } : {}) });
188
+ }
189
+ read(operationId) {
190
+ let db;
191
+ try {
192
+ db = this.open(operationId);
193
+ const frames = this.frames(db, operationId);
194
+ return this.view(db, frames.at(-1).state);
195
+ }
196
+ catch (e) {
197
+ return sqlError(e);
198
+ }
199
+ finally {
200
+ db?.close();
201
+ }
202
+ }
203
+ /** Mutator runs synchronously under the journal transaction; never awaits external work. */
204
+ update(operationId, event, mutate) {
205
+ let db;
206
+ try {
207
+ db = this.open(operationId, true);
208
+ db.exec('BEGIN IMMEDIATE');
209
+ const frames = this.frames(db, operationId), last = frames.at(-1);
210
+ const state = operationClone(last.state);
211
+ const putResult = (value) => {
212
+ const ref = randomUUID(), body = operationJson(value);
213
+ db.prepare('INSERT INTO results(ref,body,checksum) VALUES(?,?,?)').run(ref, body, operationDigest(value));
214
+ return ref;
215
+ };
216
+ mutate(state, putResult);
217
+ // Identity and accepted input are immutable across attempts and recovery actions.
218
+ for (const key of ['operationId', 'requestKey', 'scope', 'inputDigest', 'createdAt', 'generation', 'adapterRef']) {
219
+ if (state[key] !== last.state[key])
220
+ throw new OperationError('conflict', 'Operation identity cannot be changed');
221
+ }
222
+ if (operationDigest(state.input) !== state.inputDigest || operationDigest(state.binding) !== operationDigest(last.state.binding) || operationDigest(state.capabilities) !== operationDigest(last.state.capabilities) || operationDigest(state.links) !== operationDigest(last.state.links))
223
+ throw new OperationError('conflict', 'Operation binding cannot be changed');
224
+ state.revision = last.sequence + 1;
225
+ state.updatedAt = Date.now();
226
+ this.append(db, state, event, last.checksum);
227
+ db.exec('COMMIT');
228
+ return this.view(db, state);
229
+ }
230
+ catch (e) {
231
+ try {
232
+ db?.exec('ROLLBACK');
233
+ }
234
+ catch { }
235
+ return sqlError(e);
236
+ }
237
+ finally {
238
+ db?.close();
239
+ }
240
+ }
241
+ /** Only the lock holder may perform writable journal recovery after a crash. */
242
+ acquire(operationId) {
243
+ const dir = this.directory(operationId), path = join(dir, 'executor.sqlite');
244
+ if (!existsSync(path) || lstatSync(path).isSymbolicLink())
245
+ throw new OperationError('history_unavailable', 'Operation execution lock is missing');
246
+ let db;
247
+ try {
248
+ db = database(path, { timeout: 0, allowExtension: false });
249
+ db.exec('BEGIN IMMEDIATE');
250
+ const journal = this.open(operationId, true);
251
+ try {
252
+ this.frames(journal, operationId);
253
+ }
254
+ finally {
255
+ journal.close();
256
+ }
257
+ let released = false;
258
+ return () => { if (!released) {
259
+ released = true;
260
+ try {
261
+ db.exec('ROLLBACK');
262
+ }
263
+ finally {
264
+ db.close();
265
+ }
266
+ } };
267
+ }
268
+ catch (e) {
269
+ db?.close();
270
+ return sqlError(e);
271
+ }
272
+ }
273
+ events(operationId, cursor) {
274
+ let db;
275
+ try {
276
+ db = this.open(operationId);
277
+ const frames = this.frames(db, operationId), state = frames.at(-1).state;
278
+ let sequence = 0;
279
+ if (cursor) {
280
+ let decoded;
281
+ try {
282
+ decoded = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf8'));
283
+ }
284
+ catch {
285
+ throw new OperationError('invalid_cursor', 'Invalid operation event cursor');
286
+ }
287
+ if (decoded.scope !== this.scope || decoded.operationId !== operationId || decoded.generation !== state.generation || !Number.isSafeInteger(decoded.sequence) || decoded.sequence < 0 || decoded.sequence > state.revision)
288
+ throw new OperationError('invalid_cursor', 'Cursor does not belong to this operation history');
289
+ sequence = decoded.sequence;
290
+ }
291
+ return { events: operationFreeze(frames.filter(f => f.sequence > sequence)), nextCursor: Buffer.from(JSON.stringify({ scope: this.scope, operationId, generation: state.generation, sequence: state.revision })).toString('base64url') };
292
+ }
293
+ catch (e) {
294
+ return sqlError(e);
295
+ }
296
+ finally {
297
+ db?.close();
298
+ }
299
+ }
300
+ }
@@ -0,0 +1,171 @@
1
+ import type { JsonValue, CommandOutcome } from '../commandAdapter.js';
2
+ export type OperationBinding = Readonly<{
3
+ adapterId: string;
4
+ adapterVersion: string;
5
+ targetIdentity: string;
6
+ definitionDigest: string;
7
+ }>;
8
+ export type OperationCapabilities = Readonly<{
9
+ replay: 'never' | 'read_only' | 'idempotent';
10
+ query: boolean;
11
+ idempotency?: {
12
+ scope: string;
13
+ retentionMs: number;
14
+ };
15
+ }>;
16
+ export type OperationContext = Readonly<{
17
+ operationId: string;
18
+ requestKey: string;
19
+ scope: string;
20
+ input: JsonValue;
21
+ inputDigest: string;
22
+ createdAt: number;
23
+ binding: OperationBinding;
24
+ capabilities: OperationCapabilities;
25
+ /** Handle returned by the trusted adapter for the original remote task. */
26
+ remoteHandle?: string;
27
+ }>;
28
+ export type OperationAdapterResult = {
29
+ outcome: CommandOutcome;
30
+ result?: JsonValue;
31
+ process?: JsonValue;
32
+ /** Only an actual pre-dispatch failure can assert this. */
33
+ dispatch?: 'not_dispatched' | 'dispatched';
34
+ /** Returning an opaque tool result need not verify its business effects. */
35
+ execution?: 'succeeded' | 'failed';
36
+ };
37
+ export type OperationInvocation = {
38
+ operation: OperationContext;
39
+ attemptId: string;
40
+ signal: AbortSignal;
41
+ recovering: boolean;
42
+ previouslyDispatched: boolean;
43
+ /** Commit each actual transport dispatch before sending, including internal retries. */
44
+ recordDispatch?: (dispatchId: string) => void;
45
+ };
46
+ export type OperationAdapter = {
47
+ binding: OperationBinding;
48
+ capabilities: OperationCapabilities;
49
+ execute(invocation: OperationInvocation): Promise<OperationAdapterResult>;
50
+ queryOutcome?(invocation: OperationInvocation): Promise<OperationAdapterResult>;
51
+ verifyIdentity?(operation: OperationContext, signal: AbortSignal): Promise<{
52
+ targetIdentity: string;
53
+ adapterVersion: string;
54
+ } | null>;
55
+ };
56
+ export type OperationExecution = 'accepted' | 'dispatching' | 'running' | 'waiting_permission' | 'succeeded' | 'failed' | 'interrupted' | 'cancelled';
57
+ export type OperationOwner = {
58
+ pid: number;
59
+ token: string;
60
+ processStartedAt: number;
61
+ };
62
+ export type OperationAttempt = {
63
+ attemptId: string;
64
+ kind: 'execute' | 'retry' | 'query';
65
+ startedAt: number;
66
+ endedAt?: number;
67
+ status: 'dispatching' | 'returned' | 'unknown';
68
+ owner: OperationOwner;
69
+ dispatches?: Array<{
70
+ dispatchId: string;
71
+ at: number;
72
+ }>;
73
+ };
74
+ export type OperationAction = {
75
+ actionId: string;
76
+ fingerprint: string;
77
+ action: 'retry' | 'accept_verified_result' | 'reconcile' | 'cancel';
78
+ state: 'accepted' | 'waiting_permission' | 'finished' | 'unknown';
79
+ resultRevision?: number;
80
+ };
81
+ export type OperationState = Omit<OperationContext, 'remoteHandle'> & {
82
+ remoteHandle?: string;
83
+ schemaVersion: 1;
84
+ generation: string;
85
+ adapterRef: string;
86
+ revision: number;
87
+ execution: OperationExecution;
88
+ effect: 'not_dispatched' | 'unknown' | 'confirmed';
89
+ cancellation: 'none' | 'requested' | 'confirmed';
90
+ links: {
91
+ sessionId?: string;
92
+ toolUseId?: string;
93
+ taskId?: string;
94
+ toolInputDigest?: string;
95
+ };
96
+ attempts: OperationAttempt[];
97
+ actions: OperationAction[];
98
+ resultRef?: string;
99
+ receiptRef?: string;
100
+ owner?: OperationOwner;
101
+ reason?: string;
102
+ updatedAt: number;
103
+ };
104
+ export type OperationView = OperationState & {
105
+ result?: JsonValue;
106
+ receipt?: JsonValue;
107
+ allowedActions: Array<'execute' | 'get' | 'events' | 'reconcile' | 'retry' | 'accept_verified_result' | 'cancel'>;
108
+ };
109
+ export type OperationEvent = {
110
+ schemaVersion: 1;
111
+ operationId: string;
112
+ sequence: number;
113
+ previousHash: string;
114
+ checksum: string;
115
+ event: string;
116
+ state: OperationState;
117
+ };
118
+ export type OperationErrorCode = 'not_found' | 'history_unavailable' | 'unsupported_version' | 'recovery_required' | 'busy' | 'conflict' | 'invalid_cursor' | 'authorization_required' | 'permission_denied' | 'capability_changed' | 'unsupported' | 'cancelled';
119
+ export declare class OperationError extends Error {
120
+ readonly code: OperationErrorCode;
121
+ constructor(code: OperationErrorCode, message: string);
122
+ }
123
+ export type OperationPermissionRequest = {
124
+ operation: OperationContext;
125
+ action: 'read' | 'execute' | 'query' | 'retry' | 'cancel';
126
+ signal?: AbortSignal;
127
+ };
128
+ export type OperationPermissionDecision = {
129
+ behavior: 'allow' | 'deny' | 'ask';
130
+ reason?: string;
131
+ };
132
+ export type OperationHostOptions = {
133
+ storeDir: string;
134
+ scope: string;
135
+ resolveAdapter: (adapterRef: string) => Promise<OperationAdapter | undefined>;
136
+ authorize: (request: OperationPermissionRequest) => Promise<OperationPermissionDecision>;
137
+ /** Bounds host callbacks as well as adapter invocations. */
138
+ timeoutMs?: number;
139
+ };
140
+ export type OperationIdentity = {
141
+ operationId: string;
142
+ };
143
+ export type OperationMutation = OperationIdentity & {
144
+ expectedRevision: number;
145
+ actionId: string;
146
+ signal?: AbortSignal;
147
+ };
148
+ export interface OperationHost {
149
+ submitOperation(input: {
150
+ requestKey: string;
151
+ adapterRef: string;
152
+ input: JsonValue;
153
+ links?: OperationState['links'];
154
+ }): Promise<OperationView>;
155
+ executeOperation(input: OperationIdentity & {
156
+ expectedRevision: number;
157
+ signal?: AbortSignal;
158
+ }): Promise<OperationView>;
159
+ getOperation(input: OperationIdentity): Promise<OperationView>;
160
+ readOperationEvents(input: OperationIdentity & {
161
+ cursor?: string;
162
+ }): Promise<{
163
+ events: OperationEvent[];
164
+ nextCursor: string;
165
+ }>;
166
+ reconcileOperation(input: OperationMutation): Promise<OperationView>;
167
+ resumeOperation(input: OperationMutation & {
168
+ action: 'retry' | 'accept_verified_result';
169
+ }): Promise<OperationView>;
170
+ cancelOperation(input: OperationMutation): Promise<OperationView>;
171
+ }
@@ -0,0 +1,8 @@
1
+ export class OperationError extends Error {
2
+ code;
3
+ constructor(code, message) {
4
+ super(message);
5
+ this.code = code;
6
+ this.name = 'OperationError';
7
+ }
8
+ }
@@ -0,0 +1,35 @@
1
+ import { z } from 'zod/v4';
2
+ import type { PendingApproval } from './types.js';
3
+ export declare const pendingApprovalIdentitySchema: z.ZodObject<{
4
+ sessionId: z.ZodString;
5
+ requestId: z.ZodString;
6
+ toolUseId: z.ZodString;
7
+ toolName: z.ZodString;
8
+ inputDigest: z.ZodString;
9
+ }, z.core.$strict>;
10
+ export declare const pendingApprovalSuspensionResponseSchema: z.ZodObject<{
11
+ suspended: z.ZodLiteral<true>;
12
+ approval: z.ZodObject<{
13
+ sessionId: z.ZodString;
14
+ requestId: z.ZodString;
15
+ toolUseId: z.ZodString;
16
+ toolName: z.ZodString;
17
+ inputDigest: z.ZodString;
18
+ }, z.core.$strict>;
19
+ }, z.core.$strict>;
20
+ export declare const pendingApprovalConflictSchema: z.ZodObject<{
21
+ code: z.ZodLiteral<"resume_approval_mismatch">;
22
+ approval: z.ZodObject<{
23
+ sessionId: z.ZodString;
24
+ requestId: z.ZodString;
25
+ toolUseId: z.ZodString;
26
+ toolName: z.ZodString;
27
+ inputDigest: z.ZodString;
28
+ }, z.core.$strict>;
29
+ originalNotExecuted: z.ZodBoolean;
30
+ }, z.core.$strict>;
31
+ export type PendingApprovalIdentity = z.infer<typeof pendingApprovalIdentitySchema>;
32
+ /** JSON wire equality: object order is irrelevant; array order and every value matter. */
33
+ export declare function approvalInputDigest(input: Record<string, unknown>): string;
34
+ export declare function pendingApprovalIdentity(pending: PendingApproval): PendingApprovalIdentity;
35
+ export declare function samePendingApproval(expected: PendingApprovalIdentity, pending: PendingApproval): boolean;
@@ -0,0 +1,27 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { z } from 'zod/v4';
3
+ export const pendingApprovalIdentitySchema = z.strictObject({
4
+ sessionId: z.string().min(1), requestId: z.string().min(1), toolUseId: z.string().min(1),
5
+ toolName: z.string().min(1), inputDigest: z.string().regex(/^[a-f0-9]{64}$/),
6
+ });
7
+ export const pendingApprovalSuspensionResponseSchema = z.strictObject({
8
+ suspended: z.literal(true), approval: pendingApprovalIdentitySchema,
9
+ });
10
+ export const pendingApprovalConflictSchema = z.strictObject({
11
+ code: z.literal('resume_approval_mismatch'), approval: pendingApprovalIdentitySchema, originalNotExecuted: z.boolean(),
12
+ });
13
+ /** JSON wire equality: object order is irrelevant; array order and every value matter. */
14
+ export function approvalInputDigest(input) {
15
+ const json = JSON.parse(JSON.stringify(input));
16
+ const canonical = (value) => Array.isArray(value) ? value.map(canonical)
17
+ : value && typeof value === 'object' ? Object.fromEntries(Object.entries(value).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([key, child]) => [key, canonical(child)])) : value;
18
+ return createHash('sha256').update(JSON.stringify(canonical(json))).digest('hex');
19
+ }
20
+ export function pendingApprovalIdentity(pending) {
21
+ return { sessionId: pending.sessionId, requestId: pending.requestId, toolUseId: pending.toolUseId,
22
+ toolName: pending.toolName, inputDigest: approvalInputDigest(pending.input) };
23
+ }
24
+ export function samePendingApproval(expected, pending) {
25
+ const actual = pendingApprovalIdentity(pending);
26
+ return Object.keys(actual).every(key => actual[key] === expected[key]);
27
+ }
@@ -0,0 +1,19 @@
1
+ import { z } from 'zod/v4';
2
+ /** Serializable task-scoped grants. Persistent settings are reloaded by the CLI. */
3
+ export declare const permissionDelegationSchema: z.ZodObject<{
4
+ version: z.ZodLiteral<1>;
5
+ mode: z.ZodEnum<{
6
+ plan: "plan";
7
+ default: "default";
8
+ auto: "auto";
9
+ acceptEdits: "acceptEdits";
10
+ dontAsk: "dontAsk";
11
+ bypassPermissions: "bypassPermissions";
12
+ }>;
13
+ allow: z.ZodArray<z.ZodString>;
14
+ deny: z.ZodArray<z.ZodString>;
15
+ ask: z.ZodArray<z.ZodString>;
16
+ additionalDirectories: z.ZodArray<z.ZodString>;
17
+ omittedSources: z.ZodArray<z.ZodString>;
18
+ }, z.core.$strict>;
19
+ export type PermissionDelegation = z.infer<typeof permissionDelegationSchema>;
@@ -0,0 +1,11 @@
1
+ import { z } from 'zod/v4';
2
+ /** Serializable task-scoped grants. Persistent settings are reloaded by the CLI. */
3
+ export const permissionDelegationSchema = z.strictObject({
4
+ version: z.literal(1),
5
+ mode: z.enum(['default', 'acceptEdits', 'dontAsk', 'bypassPermissions', 'plan', 'auto']),
6
+ allow: z.array(z.string()),
7
+ deny: z.array(z.string()),
8
+ ask: z.array(z.string()),
9
+ additionalDirectories: z.array(z.string()),
10
+ omittedSources: z.array(z.string()),
11
+ });
@@ -20,6 +20,7 @@ type PermissionDecisionWire = {
20
20
  message: string;
21
21
  interaction?: InteractionResponse;
22
22
  interrupt?: boolean;
23
+ cancellation?: import('./cancellation.js').HostCancellation;
23
24
  toolUseID: string;
24
25
  };
25
26
  type CallbackOutcome = {
@@ -59,7 +60,8 @@ export declare function query(prompt: string, options?: QueryOptions): AsyncGene
59
60
  *
60
61
  * On allow, the suspended tool executes with exactly the recorded input (or
61
62
  * decision.updatedInput if the owner amended it); on deny the model receives
62
- * the denial and the session continues.
63
+ * the denial and the session continues. With expectedApproval, both the saved
64
+ * identity and regenerated input must match; substitution is not permitted.
63
65
  */
64
66
  export declare function resumeWithDecision(sessionId: string, decision: PendingDecision, options?: QueryOptions): AsyncGenerator<SDKEvent>;
65
67
  /** P1-7: accumulated usage for a session, across all queries that ran it. */