@myagentroam/agent 0.10.24 → 0.10.26

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 (36) hide show
  1. package/NOTICE +26 -0
  2. package/dist/agent-core/extensions/operation.d.ts +1 -0
  3. package/dist/agent-core/extensions/operation.js +3 -0
  4. package/dist/agent-core/extensions/session.js +14 -0
  5. package/dist/agent-core/host/contracts.d.ts +1 -0
  6. package/dist/agent-core/host/local-agent-host.d.ts +2 -0
  7. package/dist/agent-core/host/local-agent-host.js +2 -0
  8. package/dist/agent-core/model/contracts.d.ts +2 -0
  9. package/dist/agent-core/model/openai-responses.js +8 -2
  10. package/dist/agent-core/prompts/compact.js +1 -0
  11. package/dist/agent-core/prompts/permissions.d.ts +2 -0
  12. package/dist/agent-core/prompts/permissions.js +17 -0
  13. package/dist/agent-core/prompts/workflow.d.ts +1 -1
  14. package/dist/agent-core/prompts/workflow.js +1 -1
  15. package/dist/agent-core/runtime/contracts.d.ts +2 -0
  16. package/dist/agent-core/runtime/events.d.ts +4 -0
  17. package/dist/agent-core/runtime/execution.d.ts +1 -0
  18. package/dist/agent-core/runtime/execution.js +5 -0
  19. package/dist/agent-core/sdk/session.d.ts +1 -0
  20. package/dist/agent-core/sdk/session.js +54 -16
  21. package/dist/agent-core/subagent/contracts.d.ts +1 -0
  22. package/dist/agent-core/subagent/scheduler.d.ts +4 -0
  23. package/dist/agent-core/subagent/session-controller.js +13 -0
  24. package/dist/agent-core/tools/code-mode/cells.js +11 -1
  25. package/dist/agent-core/tools/code-mode/dispatch.js +5 -0
  26. package/dist/agent-core/tools/code-mode/index.js +26 -13
  27. package/dist/agent-core/tools/code-mode/quickjs-engine.js +2 -0
  28. package/dist/agent-core/tools/contracts.d.ts +6 -0
  29. package/dist/agent-core/tools/execution.js +7 -0
  30. package/dist/agent-core/tools/output-projection.js +5 -0
  31. package/dist/agent-core/tools/permissions.d.ts +85 -0
  32. package/dist/agent-core/tools/permissions.js +237 -0
  33. package/dist/agent-core/tools/runtime-context.d.ts +2 -0
  34. package/dist/cli/main.js +13 -2
  35. package/dist/index.d.ts +1 -0
  36. package/package.json +1 -1
@@ -522,6 +522,7 @@ export class SubagentSessionController {
522
522
  state.status = 'queued';
523
523
  this.#touchResource(state);
524
524
  delete state.summary;
525
+ delete state.permissionStop;
525
526
  delete state.errorCode;
526
527
  delete state.errorMessage;
527
528
  state.changedFiles = [];
@@ -613,6 +614,10 @@ export class SubagentSessionController {
613
614
  if (result.status === 'completed') {
614
615
  state.status = 'completed';
615
616
  state.summary = result.finalAnswer;
617
+ if (result.permissionStop)
618
+ state.permissionStop = result.permissionStop;
619
+ else
620
+ delete state.permissionStop;
616
621
  }
617
622
  else if (result.status === 'cancelled')
618
623
  state.status = 'cancelled';
@@ -855,6 +860,9 @@ export class SubagentSessionController {
855
860
  ...(state.extensionId === undefined ? {} : { extensionId: state.extensionId })
856
861
  }
857
862
  : {}),
863
+ ...(includeTerminalDetails && state.permissionStop
864
+ ? { permissionStop: state.permissionStop }
865
+ : {}),
858
866
  ...state.receipt,
859
867
  ...(state.modelId === undefined ? {} : { modelId: state.modelId }),
860
868
  ...(state.modelName === undefined ? {} : { modelName: state.modelName }),
@@ -1017,6 +1025,11 @@ function restoreAgentState(session) {
1017
1025
  terminalObserved: false,
1018
1026
  resourceRevision: 0
1019
1027
  };
1028
+ if (terminalType === 'execution.completed' && terminal) {
1029
+ const event = terminal.payload;
1030
+ if (event.permissionStop)
1031
+ state.permissionStop = event.permissionStop;
1032
+ }
1020
1033
  for (const record of currentRecords) {
1021
1034
  if (!isRecord(record.payload) ||
1022
1035
  !['message.completed', 'tool.started', 'tool.completed', 'tool.failed'].includes(eventType(record) ?? ''))
@@ -1,4 +1,5 @@
1
1
  import { randomUUID } from 'node:crypto';
2
+ import { permissionStopFromError } from '../permissions.js';
2
3
  import { MarAgentError, describeMarAgentError } from '../../error.js';
3
4
  import { truncateModelText } from '../../runtime/token-budget.js';
4
5
  import { TOOL_EXECUTION_LIMITS as limits } from '../execution-limits.js';
@@ -122,7 +123,13 @@ export class CodeCells {
122
123
  })
123
124
  .then(async () => {
124
125
  try {
125
- await context.codeMode?.onSettled?.({ cellId: cell.cellId, status: outcome });
126
+ await context.codeMode?.onSettled?.({
127
+ cellId: cell.cellId,
128
+ status: outcome,
129
+ ...(permissionStopFromError(cell.error)
130
+ ? { permissionStop: permissionStopFromError(cell.error) }
131
+ : {})
132
+ });
126
133
  }
127
134
  catch (error) {
128
135
  cell.error = error;
@@ -263,6 +270,9 @@ export class CodeCells {
263
270
  const data = {
264
271
  cellId: cell.cellId,
265
272
  status: cell.status,
273
+ ...(permissionStopFromError(cell.error)
274
+ ? { permissionStop: permissionStopFromError(cell.error) }
275
+ : {}),
266
276
  output: bounded.content,
267
277
  observations: remapped,
268
278
  calls: [...cell.calls.values()],
@@ -5,6 +5,7 @@ import { allowsToolCaller } from '../callers.js';
5
5
  import { boundedModelToolOutput, structuredToolEventData, structuredToolFailureData } from '../output-projection.js';
6
6
  import { executeRegisteredTool } from '../runtime.js';
7
7
  import { failedToolResultExtensions } from '../result-extensions.js';
8
+ import { permissionStopFromError } from '../permissions.js';
8
9
  /** Capture only the cell's immutable dispatch identity, never the model loop or its AbortSignal. */
9
10
  export function createCodeToolDispatch(input) {
10
11
  const definitions = new Map(input.tools.map((tool) => [tool.name, tool]));
@@ -25,6 +26,7 @@ export function createCodeToolDispatch(input) {
25
26
  const expected = input.expectations.get(call.name);
26
27
  const result = await executeRegisteredTool(call.name, call.arguments, callId, {
27
28
  ...input.context,
29
+ parentCallId: input.parentCallId,
28
30
  signal,
29
31
  ...(expected ? { expectedMcpTool: expected } : {})
30
32
  }, 'programmatic');
@@ -65,6 +67,9 @@ export function createCodeToolDispatch(input) {
65
67
  ...identity,
66
68
  code: diagnostic.code,
67
69
  message: diagnostic.message,
70
+ ...(permissionStopFromError(error)
71
+ ? { permissionStop: permissionStopFromError(error) }
72
+ : {}),
68
73
  ...(data === undefined ? {} : { data })
69
74
  });
70
75
  throw error;
@@ -1,4 +1,5 @@
1
1
  import { randomUUID } from 'node:crypto';
2
+ import { isToolPermissionFailure, permissionStopFromError, ToolPermissionStopError } from '../permissions.js';
2
3
  import { readFile } from 'node:fs/promises';
3
4
  import { createRequire } from 'node:module';
4
5
  import { dirname, sep } from 'node:path';
@@ -31,9 +32,9 @@ export const codeModeToolDefinition = {
31
32
  - Use only standard ECMAScript built-ins and the explicitly documented tools, observe, state, catalog and cell namespaces. Do not use Node.js, browser or Web APIs. Module imports, filesystem and network APIs are unavailable. Obtain external capabilities only through available tools. Use tools.exec to start managed processes when permitted; use tools.exec_wait afterward to wait, write stdin or cancel.
32
33
 
33
34
  Tool calls:
34
- - tools.NAME(input) returns a Promise. Use the existing tool input schema; freeform tools such as apply_patch accept a raw string, not a JSON wrapper. For parameterless tools prefer tools.NAME({}); tools.NAME() is accepted only for explicitly closed empty-object schemas.
35
+ - tools.NAME(input) returns a Promise. Use the current catalog input schema: raw text only when inputMode is freeform; use the declared JSON form when inputMode is json. For parameterless tools prefer tools.NAME({}); tools.NAME() is accepted only for explicitly closed empty-object schemas.
35
36
  - Results use the MAR envelope: { content?: string, data?: unknown, images?: Array<{mimeType:string,dataBase64:string}>, truncated?: boolean }. Redundant JSON content is omitted when it exactly renders data; independent text remains in content. Prefer data for programmatic results: result.data.results[0].text for read source text and result.data.exitCode/result.data.stdout for exec. content, when present, is rendered text, not raw file bytes. Use only fields documented by that tool; do not invent success or ok flags.
36
- - Tool errors reject with code/message and may be caught. A failed cell preserves already emitted output alongside its status and error; completed tool effects are not rolled back. Inspect it before retrying completed work. Await every call; unawaited calls fail the cell. Use Promise.all for independent calls and sequential await for dependencies. Runtime preserves safe/serial barriers and Host checks.
37
+ - Tool errors reject with code/message and may be caught, except nested Auto deny, which skips only that call and resolves with data.error/data.message. A failed cell preserves already emitted output alongside its status and error; completed tool effects are not rolled back. Inspect it before retrying completed work. Await every call; unawaited calls fail the cell. Use Promise.all for independent calls and sequential await for dependencies. Runtime preserves safe/serial barriers and Host checks.
37
38
  - skill, tool_search, question, code_exec, code_exec_wait and direct-only tools cannot be called here. Load Skills directly and read their instructions before the next tool call. Use direct tool_search before a later code cell for deferred tools; the current cell's tools do not change.
38
39
 
39
40
  Built-in namespaces:
@@ -525,18 +526,26 @@ export class CodeModeTool {
525
526
  : {}),
526
527
  error: { code: diagnostic.code, message: diagnostic.message }
527
528
  });
528
- toolResults.set(operationId, {
529
- source,
530
- outcome: 'failed',
531
- result: {
532
- content: JSON.stringify({
533
- error: diagnostic.code,
534
- message: diagnostic.message,
535
- ...(data === undefined ? {} : { data })
536
- }),
529
+ const failureResult = {
530
+ content: JSON.stringify({
531
+ error: diagnostic.code,
532
+ message: diagnostic.message,
537
533
  ...(data === undefined ? {} : { data })
538
- }
539
- });
534
+ }),
535
+ ...(data === undefined ? {} : { data })
536
+ };
537
+ const permissionStop = permissionStopFromError(error);
538
+ if (isToolPermissionFailure(error))
539
+ failureResult.data = { error: diagnostic.code, message: diagnostic.message };
540
+ toolResults.set(operationId, { source, outcome: 'failed', result: failureResult });
541
+ if (isToolPermissionFailure(error)) {
542
+ await write({ id, ok: true, output: { data: failureResult.data } });
543
+ return;
544
+ }
545
+ if (permissionStop) {
546
+ fail(error);
547
+ return;
548
+ }
540
549
  await write({
541
550
  id,
542
551
  ok: false,
@@ -625,6 +634,7 @@ export class CodeModeTool {
625
634
  name: tool.name,
626
635
  description: tool.description,
627
636
  inputSchema: tool.inputSchema,
637
+ ...(tool.parameterless ? { parameterless: true } : {}),
628
638
  inputMode: tool.inputMode ?? 'json'
629
639
  })),
630
640
  state: context.codeMode.state ?? [],
@@ -643,6 +653,9 @@ export class CodeModeTool {
643
653
  }
644
654
  });
645
655
  await exit;
656
+ const permissionStop = context.codeMode?.permissionStop?.();
657
+ if (permissionStop)
658
+ throw new ToolPermissionStopError(permissionStop);
646
659
  if (failure !== undefined)
647
660
  throw failure;
648
661
  if (!complete)
@@ -749,6 +749,8 @@ function compilerCaret(source, prefix, endOfInput = false) {
749
749
  return ` (code-mode.js:${line}${endOfInput ? ', end of input' : ''})\nSyntax location: line ${line}, column ${column}\n${line} | ${preview}${token}\n${' '.repeat(String(line).length)} | ${' '.repeat(preview.length)}^`;
750
750
  }
751
751
  function isParameterlessTool(tool) {
752
+ if (tool.parameterless === true)
753
+ return true;
752
754
  const schema = tool.inputSchema;
753
755
  return (tool.inputMode === 'json' &&
754
756
  schema?.type === 'object' &&
@@ -12,6 +12,8 @@ export interface ClientToolDefinition {
12
12
  allowedCallers?: readonly ToolCaller[];
13
13
  inputMode?: 'json' | 'freeform';
14
14
  parallelSafety?: 'safe' | 'serial';
15
+ /** Preserved business-schema eligibility after framework metadata projection. */
16
+ parameterless?: boolean;
15
17
  /** Runtime-only Agent catalog scope; never forwarded as an upstream tool field. */
16
18
  agentScope?: 'ROOT_AND_SUBAGENTS' | 'ROOT_ONLY';
17
19
  }
@@ -19,6 +21,8 @@ export interface ToolCall {
19
21
  name: string;
20
22
  arguments: unknown;
21
23
  callId?: string;
24
+ /** Framework metadata for native invocations, never a business argument. */
25
+ permission?: import('./permissions.js').ToolPermission | null;
22
26
  }
23
27
  export interface ToolExecutionContext {
24
28
  mode: ExecutionMode;
@@ -35,6 +39,7 @@ export interface ToolExecutionContext {
35
39
  retainSession?(): Promise<() => Promise<void>>;
36
40
  onSettled?(result: CodeCellEventData): Promise<void>;
37
41
  supportsImages?: boolean;
42
+ permissionStop?(): import('./permissions.js').PermissionStop | undefined;
38
43
  tools: readonly ClientToolDefinition[];
39
44
  state?: readonly CodeModeStateEntry[];
40
45
  commitState?(mutations: readonly CodeModeStateMutation[]): Promise<readonly CodeModeStateEntry[]>;
@@ -99,6 +104,7 @@ export interface ToolExecutionResult {
99
104
  resultExtensions?: readonly import('./result-extensions.js').ToolResultExtension[];
100
105
  }
101
106
  export interface CodeCellEventData {
107
+ permissionStop?: import('./permissions.js').PermissionStop;
102
108
  calls?: readonly import('./code-mode/observations.js').CodeToolCallRecord[];
103
109
  cellId: string;
104
110
  status: 'running' | 'completed' | 'failed' | 'cancelled';
@@ -6,6 +6,7 @@ import { createCodeToolDispatch } from './code-mode/dispatch.js';
6
6
  import { boundedModelToolOutput, structuredToolArtifacts, structuredToolEventData, structuredToolFailureData } from './output-projection.js';
7
7
  import { executeRegisteredTool } from './runtime.js';
8
8
  import { failedToolResultExtensions } from './result-extensions.js';
9
+ import { permissionStopFromError } from './permissions.js';
9
10
  import { scheduleToolCallBatch } from './scheduler.js';
10
11
  /** Shared raw context construction for physical tools and native calls in a real model round. */
11
12
  export async function createRoundToolRuntimeContext(scope, call, signal) {
@@ -131,6 +132,9 @@ export async function executeRoundTools(calls, scope) {
131
132
  ...(parentCallId === undefined ? {} : { parentCallId }),
132
133
  code: diagnostic.code,
133
134
  message: diagnostic.message,
135
+ ...(permissionStopFromError(error)
136
+ ? { permissionStop: permissionStopFromError(error) }
137
+ : {}),
134
138
  ...(eventData === undefined ? {} : { data: eventData })
135
139
  });
136
140
  return {
@@ -139,6 +143,9 @@ export async function executeRoundTools(calls, scope) {
139
143
  ? boundedModelToolOutput(call.name, JSON.stringify({
140
144
  error: diagnostic.code,
141
145
  message: diagnostic.message,
146
+ ...(permissionStopFromError(error)
147
+ ? { permissionStop: permissionStopFromError(error) }
148
+ : {}),
142
149
  ...(failureData === undefined ? {} : { data: failureData }),
143
150
  ...(error instanceof MarAgentError && error.retryable ? { retryable: true } : {})
144
151
  }), failureData).content
@@ -42,6 +42,11 @@ export function structuredToolEventData(toolName, value) {
42
42
  cellId: value.cellId,
43
43
  status: value.status,
44
44
  cached: value.cached === true,
45
+ ...(isRecord(value.permissionStop)
46
+ ? {
47
+ permissionStop: value.permissionStop
48
+ }
49
+ : {}),
45
50
  ...(Array.isArray(value.calls)
46
51
  ? {
47
52
  calls: value.calls.filter((entry) => isRecord(entry) &&
@@ -0,0 +1,85 @@
1
+ import { MarAgentError } from '../error.js';
2
+ import type { ClientToolDefinition, ToolCall } from './contracts.js';
3
+ export type PermissionMode = 'auto' | 'full-access';
4
+ export interface ToolPermission {
5
+ action: 'allow' | 'ask' | 'deny';
6
+ reason?: string | null;
7
+ }
8
+ export interface PermissionStop {
9
+ action: 'ask' | 'deny';
10
+ reason: string;
11
+ source: {
12
+ sessionId: string;
13
+ executionId: string;
14
+ callId: string;
15
+ toolName: string;
16
+ parentCallId?: string;
17
+ };
18
+ }
19
+ export interface ToolApprovalRequest {
20
+ readonly approvalId: string;
21
+ readonly reason: string;
22
+ readonly source: PermissionStop['source'];
23
+ readonly input: unknown;
24
+ readonly expiresAt: number;
25
+ }
26
+ export type ToolApprovalDecision = 'APPROVED' | 'REJECTED';
27
+ export type ToolApprovalHandler = (request: ToolApprovalRequest, signal: AbortSignal) => Promise<ToolApprovalDecision>;
28
+ type InvocationContext = {
29
+ sessionId: string;
30
+ executionId?: string;
31
+ actor: string;
32
+ entry?: string;
33
+ parentToolCallId?: string;
34
+ signal?: AbortSignal;
35
+ };
36
+ export declare const permissionSchema: {
37
+ type: string;
38
+ properties: {
39
+ action: {
40
+ type: string;
41
+ enum: string[];
42
+ };
43
+ reason: {
44
+ type: string;
45
+ description: string;
46
+ };
47
+ };
48
+ required: string[];
49
+ additionalProperties: boolean;
50
+ };
51
+ export declare function resolvePermissionMode(value: unknown): PermissionMode;
52
+ export declare function projectPermissionTool(tool: ClientToolDefinition): ClientToolDefinition;
53
+ export declare class ToolPermissionStopError extends MarAgentError {
54
+ readonly permissionStop: PermissionStop;
55
+ constructor(permissionStop: PermissionStop);
56
+ }
57
+ /** One captured Execution's policy, including its retained tool frames. Not an authorization ledger. */
58
+ export declare class ExecutionToolPermissions {
59
+ #private;
60
+ readonly mode: PermissionMode;
61
+ readonly identity: {
62
+ sessionId: string;
63
+ executionId: string;
64
+ };
65
+ private readonly approval?;
66
+ constructor(mode: PermissionMode, identity: {
67
+ sessionId: string;
68
+ executionId: string;
69
+ }, approval?: {
70
+ readonly request: ToolApprovalHandler;
71
+ readonly signal: AbortSignal;
72
+ } | undefined);
73
+ get stop(): PermissionStop | undefined;
74
+ assertDispatch(): void;
75
+ prepare(call: ToolCall, context: InvocationContext, tool: ClientToolDefinition): ToolCall;
76
+ authorize(call: ToolCall, context: InvocationContext): Promise<ToolCall>;
77
+ }
78
+ export declare function isToolPermissionFailure(error: unknown): boolean;
79
+ export declare function withToolPermissions<T>(policy: ExecutionToolPermissions, run: () => T): T;
80
+ export declare function currentToolPermissions(context?: {
81
+ sessionId: string;
82
+ executionId?: string;
83
+ }): ExecutionToolPermissions | undefined;
84
+ export declare function permissionStopFromError(error: unknown): PermissionStop | undefined;
85
+ export {};
@@ -0,0 +1,237 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { MarAgentError } from '../error.js';
4
+ const permissionContext = new AsyncLocalStorage();
5
+ const record = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
6
+ const schemaKeys = new Set([
7
+ 'type',
8
+ 'properties',
9
+ 'required',
10
+ 'additionalProperties',
11
+ 'description',
12
+ 'title',
13
+ '$schema',
14
+ '$comment'
15
+ ]);
16
+ export const permissionSchema = {
17
+ type: 'object',
18
+ properties: {
19
+ action: { type: 'string', enum: ['allow', 'ask', 'deny'] },
20
+ reason: {
21
+ type: 'string',
22
+ description: 'Brief public reason, required for ask/deny; never include secrets.'
23
+ }
24
+ },
25
+ required: ['action'],
26
+ additionalProperties: false
27
+ };
28
+ export function resolvePermissionMode(value) {
29
+ if (value === undefined)
30
+ return 'full-access';
31
+ if (value === 'auto' || value === 'full-access')
32
+ return value;
33
+ throw new MarAgentError('MAR_AGENT_TOOL_INPUT_INVALID', 'Permission mode must be auto or full-access.');
34
+ }
35
+ function flatInput(tool) {
36
+ const schema = tool.inputSchema;
37
+ return (schema.type === 'object' &&
38
+ schema.additionalProperties === false &&
39
+ record(schema.properties) &&
40
+ !Object.hasOwn(schema.properties, 'permission') &&
41
+ Object.keys(schema).every((key) => schemaKeys.has(key)));
42
+ }
43
+ export function projectPermissionTool(tool) {
44
+ const flat = flatInput(tool);
45
+ const parameterless = flat &&
46
+ Object.keys(tool.inputSchema.properties).length === 0 &&
47
+ (!Array.isArray(tool.inputSchema.required) || tool.inputSchema.required.length === 0);
48
+ return {
49
+ ...tool,
50
+ inputMode: 'json',
51
+ ...(parameterless ? { parameterless: true } : {}),
52
+ description: tool.description +
53
+ '\nAuto mode: use the declared JSON input. Omit permission (or set null) for routine or already authorized work. Use {action:"ask"|"deny",reason:"brief public reason"} only when required. ' +
54
+ (flat
55
+ ? 'Permission is metadata, not a business argument.'
56
+ : 'Place the unchanged business arguments in input; permission is separate metadata.'),
57
+ inputSchema: flat
58
+ ? {
59
+ ...tool.inputSchema,
60
+ properties: {
61
+ ...tool.inputSchema.properties,
62
+ permission: permissionSchema
63
+ }
64
+ }
65
+ : {
66
+ type: 'object',
67
+ properties: { input: tool.inputSchema, permission: permissionSchema },
68
+ required: ['input'],
69
+ additionalProperties: false
70
+ }
71
+ };
72
+ }
73
+ function parsePermission(value) {
74
+ if (value === undefined || value === null)
75
+ return;
76
+ if (!record(value) ||
77
+ Object.keys(value).some((key) => key !== 'action' && key !== 'reason') ||
78
+ !['allow', 'ask', 'deny'].includes(String(value.action)) ||
79
+ typeof value.action !== 'string' ||
80
+ (value.reason !== undefined && value.reason !== null && typeof value.reason !== 'string'))
81
+ throw new MarAgentError('MAR_AGENT_TOOL_INPUT_INVALID', 'Invalid tool permission declaration.');
82
+ const reason = typeof value.reason === 'string' ? value.reason.trim() : undefined;
83
+ if ((reason !== undefined && [...reason].length > 240) || (value.action !== 'allow' && !reason))
84
+ throw new MarAgentError('MAR_AGENT_TOOL_INPUT_INVALID', 'ask/deny requires a public reason of 1 to 240 characters.');
85
+ return {
86
+ action: value.action,
87
+ ...(reason === undefined ? {} : { reason })
88
+ };
89
+ }
90
+ export class ToolPermissionStopError extends MarAgentError {
91
+ permissionStop;
92
+ constructor(permissionStop) {
93
+ super(permissionStop.action === 'ask' ? 'MAR_AGENT_PERMISSION_ASK' : 'MAR_AGENT_PERMISSION_DENIED', permissionStop.reason);
94
+ this.permissionStop = permissionStop;
95
+ }
96
+ }
97
+ /** One captured Execution's policy, including its retained tool frames. Not an authorization ledger. */
98
+ export class ExecutionToolPermissions {
99
+ mode;
100
+ identity;
101
+ approval;
102
+ #stop;
103
+ constructor(mode, identity, approval) {
104
+ this.mode = mode;
105
+ this.identity = identity;
106
+ this.approval = approval;
107
+ }
108
+ get stop() {
109
+ return this.#stop;
110
+ }
111
+ assertDispatch() {
112
+ if (this.#stop)
113
+ throw new MarAgentError('MAR_AGENT_TOOL_SKIPPED_PERMISSION_STOP', 'Tool was not executed because this execution stopped for permission.');
114
+ }
115
+ prepare(call, context, tool) {
116
+ if (this.mode !== 'auto') {
117
+ if (call.permission !== undefined && call.permission !== null)
118
+ throw new MarAgentError('MAR_AGENT_TOOL_INPUT_INVALID', 'Framework permission metadata requires an Auto execution.');
119
+ return call;
120
+ }
121
+ this.assertDispatch();
122
+ let business = call.arguments;
123
+ let declaration = call.permission;
124
+ if (context.actor === 'model' || context.actor === 'code-mode') {
125
+ if (!record(business))
126
+ throw new MarAgentError('MAR_AGENT_TOOL_INPUT_INVALID', 'Auto tool input must be a JSON object.');
127
+ declaration = business.permission;
128
+ if (flatInput(tool)) {
129
+ const { permission: _permission, ...input } = business;
130
+ business = input;
131
+ }
132
+ else {
133
+ if (Object.keys(business).some((key) => key !== 'input' && key !== 'permission') ||
134
+ !Object.hasOwn(business, 'input'))
135
+ throw new MarAgentError('MAR_AGENT_TOOL_INPUT_INVALID', 'Auto input requires the original business arguments in input.');
136
+ business = business.input;
137
+ }
138
+ }
139
+ const permission = parsePermission(declaration);
140
+ const { permission: _permission, ...input } = call;
141
+ const prepared = { ...input, arguments: business };
142
+ if (permission?.action === 'ask' && this.approval)
143
+ return { ...prepared, permission };
144
+ if (permission && permission.action !== 'allow') {
145
+ const stop = Object.freeze({
146
+ action: permission.action,
147
+ reason: permission.reason,
148
+ source: Object.freeze({
149
+ sessionId: this.identity.sessionId,
150
+ executionId: this.identity.executionId,
151
+ callId: call.callId ?? randomUUID(),
152
+ toolName: call.name,
153
+ ...(context.parentToolCallId === undefined
154
+ ? {}
155
+ : { parentCallId: context.parentToolCallId })
156
+ })
157
+ });
158
+ if (permission.action === 'ask')
159
+ this.#stop = stop;
160
+ throw new ToolPermissionStopError(stop);
161
+ }
162
+ return prepared;
163
+ }
164
+ async authorize(call, context) {
165
+ if (call.permission?.action !== 'ask' || !this.approval)
166
+ return call;
167
+ const { permission, ...captured } = structuredClone(call);
168
+ const timeout = new AbortController();
169
+ const signal = AbortSignal.any([
170
+ this.approval.signal,
171
+ ...(context.signal ? [context.signal] : []),
172
+ timeout.signal
173
+ ]);
174
+ signal.throwIfAborted();
175
+ const request = {
176
+ approvalId: `mar-agent:${randomUUID()}`,
177
+ reason: permission.reason,
178
+ source: {
179
+ ...this.identity,
180
+ callId: call.callId ?? randomUUID(),
181
+ toolName: call.name,
182
+ ...(context.parentToolCallId ? { parentCallId: context.parentToolCallId } : {})
183
+ },
184
+ input: structuredClone(captured.arguments),
185
+ expiresAt: Date.now() + 10 * 60_000
186
+ };
187
+ const timer = setTimeout(() => timeout.abort(new MarAgentError('MAR_AGENT_APPROVAL_EXPIRED', 'Approval expired after 10 minutes without authorizing the tool call.')), 10 * 60_000);
188
+ timer.unref();
189
+ let abort = () => { };
190
+ try {
191
+ const decision = await new Promise((resolve, reject) => {
192
+ abort = () => reject(signal.reason);
193
+ signal.addEventListener('abort', abort, { once: true });
194
+ Promise.resolve()
195
+ .then(() => {
196
+ signal.throwIfAborted();
197
+ return this.approval.request(request, signal);
198
+ })
199
+ .then(resolve, reject);
200
+ });
201
+ signal.throwIfAborted();
202
+ if (Date.now() >= request.expiresAt)
203
+ throw new MarAgentError('MAR_AGENT_APPROVAL_EXPIRED', 'Approval expired after 10 minutes without authorizing the tool call.');
204
+ if (decision !== 'APPROVED')
205
+ throw new MarAgentError('MAR_AGENT_APPROVAL_REJECTED', `User rejected this tool call: ${permission.reason}`);
206
+ this.assertDispatch();
207
+ return captured;
208
+ }
209
+ finally {
210
+ clearTimeout(timer);
211
+ signal.removeEventListener('abort', abort);
212
+ }
213
+ }
214
+ }
215
+ export function isToolPermissionFailure(error) {
216
+ return (permissionStopFromError(error)?.action === 'deny' ||
217
+ (error instanceof MarAgentError &&
218
+ ['MAR_AGENT_APPROVAL_REJECTED', 'MAR_AGENT_APPROVAL_EXPIRED'].includes(error.code)));
219
+ }
220
+ export function withToolPermissions(policy, run) {
221
+ return permissionContext.run(policy, run);
222
+ }
223
+ export function currentToolPermissions(context) {
224
+ const policy = permissionContext.getStore();
225
+ return policy &&
226
+ (!context ||
227
+ (policy.identity.sessionId === context.sessionId &&
228
+ policy.identity.executionId === context.executionId))
229
+ ? policy
230
+ : undefined;
231
+ }
232
+ export function permissionStopFromError(error) {
233
+ for (let depth = 0; depth < 5 && error instanceof Error; depth++, error = error.cause)
234
+ if (error instanceof ToolPermissionStopError)
235
+ return error.permissionStop;
236
+ return;
237
+ }
@@ -46,6 +46,8 @@ export interface ToolRuntimeContext {
46
46
  executionId: string;
47
47
  modelId?: string;
48
48
  signal: AbortSignal;
49
+ permissionPolicy?: import('./permissions.js').ExecutionToolPermissions;
50
+ parentCallId?: string;
49
51
  writeToolResultResource?: import('./result-extensions.js').ToolResultResourceWriter;
50
52
  codeMode?: import('./contracts.js').ToolExecutionContext['codeMode'];
51
53
  skills: ReadonlyMap<string, AgentSkill>;
package/dist/cli/main.js CHANGED
@@ -17,7 +17,7 @@ const HELP = `maragent - MAR coding agent
17
17
 
18
18
  Usage:
19
19
  maragent [--model ID] [--reasoning-effort EFFORT] [--codex-auth-file PATH]
20
- maragent run <prompt> [--reasoning-effort EFFORT] [--format text|json|stream-json] [--codex-auth-file PATH]
20
+ maragent run <prompt> [--permission-mode auto|full-access] [--reasoning-effort EFFORT] [--format text|json|stream-json] [--codex-auth-file PATH]
21
21
  maragent resume [sessionId] [--reasoning-effort EFFORT] [--codex-auth-file PATH]
22
22
  maragent sessions list|delete <sessionId>
23
23
  maragent models list|add|edit|remove|test [id] [--default-reasoning-effort EFFORT] [--code-mode|--no-code-mode] [--fast-mode|--no-fast-mode] [--codex-auth-file PATH]
@@ -249,6 +249,12 @@ async function main() {
249
249
  ? undefined
250
250
  : parseReasoningEffort(reasoningEffortOption, '--reasoning-effort');
251
251
  const legacyAccess = takeOption(arguments_, '--access');
252
+ const permissionModeOption = takeOption(arguments_, '--permission-mode');
253
+ if (permissionModeOption !== undefined &&
254
+ permissionModeOption !== 'auto' &&
255
+ permissionModeOption !== 'full-access')
256
+ throw new MarAgentError('MAR_AGENT_CLI_ARGUMENT_INVALID', '--permission-mode must be auto or full-access.');
257
+ const permissionMode = permissionModeOption;
252
258
  if (legacyAccess !== undefined && legacyAccess !== 'full-access')
253
259
  throw new MarAgentError('MAR_AGENT_CLI_ARGUMENT_INVALID', 'The deprecated --access option only accepts full-access.');
254
260
  const format = takeOption(arguments_, '--format') ?? 'text';
@@ -340,6 +346,7 @@ async function main() {
340
346
  break;
341
347
  const invocation = parseExecutionInvocation(prompt);
342
348
  const runInput = {
349
+ ...(permissionMode === undefined ? {} : { permissionMode }),
343
350
  prompt: invocation.prompt,
344
351
  ...(modelId === undefined ? {} : { modelId }),
345
352
  ...(reasoningEffort === undefined ? {} : { reasoningEffort })
@@ -388,7 +395,11 @@ async function main() {
388
395
  if (format === 'json')
389
396
  stdout.write(`${JSON.stringify(result)}\n`);
390
397
  else if (format === 'text')
391
- stdout.write('\n');
398
+ stdout.write(result.permissionStop
399
+ ? `\nNot executed (${result.permissionStop.action}): ${result.permissionStop.reason}\n${result.permissionStop.action === 'ask' ? 'Reply with allow or a scoped instruction to continue.\n' : ''}`
400
+ : '\n');
401
+ if (!interactive && result.permissionStop)
402
+ process.exitCode = result.permissionStop.action === 'ask' ? 3 : 1;
392
403
  if (result.status !== 'completed')
393
404
  process.exitCode = cliExitCode(result.errorCode);
394
405
  } while (interactive);
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export { MarAgentError } from './agent-core/error.js';
2
2
  export { MAR_AGENT_HOST_API_VERSION, negotiateHost, type HostDescription, type MarAgentHostCapability } from './agent-core/host/contracts.js';
3
+ export type { PermissionMode, ToolApprovalRequest, ToolApprovalDecision, ToolApprovalHandler, ToolPermission, PermissionStop } from './agent-core/tools/permissions.js';
3
4
  export type { ToolResultExtension, FileChangeEvidence } from './agent-core/tools/result-extensions.js';
4
5
  export { parseToolResultExtensions } from './agent-core/tools/result-extensions.js';
5
6
  export type { ToolResultResourceInput, ToolResultResourcePage } from './agent-core/session/tool-result-resources.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myagentroam/agent",
3
- "version": "0.10.24",
3
+ "version": "0.10.26",
4
4
  "description": "Embeddable MAR coding agent SDK and CLI.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",