@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.
- package/NOTICE +26 -0
- package/dist/agent-core/extensions/operation.d.ts +1 -0
- package/dist/agent-core/extensions/operation.js +3 -0
- package/dist/agent-core/extensions/session.js +14 -0
- package/dist/agent-core/host/contracts.d.ts +1 -0
- package/dist/agent-core/host/local-agent-host.d.ts +2 -0
- package/dist/agent-core/host/local-agent-host.js +2 -0
- package/dist/agent-core/model/contracts.d.ts +2 -0
- package/dist/agent-core/model/openai-responses.js +8 -2
- package/dist/agent-core/prompts/compact.js +1 -0
- package/dist/agent-core/prompts/permissions.d.ts +2 -0
- package/dist/agent-core/prompts/permissions.js +17 -0
- package/dist/agent-core/prompts/workflow.d.ts +1 -1
- package/dist/agent-core/prompts/workflow.js +1 -1
- package/dist/agent-core/runtime/contracts.d.ts +2 -0
- package/dist/agent-core/runtime/events.d.ts +4 -0
- package/dist/agent-core/runtime/execution.d.ts +1 -0
- package/dist/agent-core/runtime/execution.js +5 -0
- package/dist/agent-core/sdk/session.d.ts +1 -0
- package/dist/agent-core/sdk/session.js +54 -16
- package/dist/agent-core/subagent/contracts.d.ts +1 -0
- package/dist/agent-core/subagent/scheduler.d.ts +4 -0
- package/dist/agent-core/subagent/session-controller.js +13 -0
- package/dist/agent-core/tools/code-mode/cells.js +11 -1
- package/dist/agent-core/tools/code-mode/dispatch.js +5 -0
- package/dist/agent-core/tools/code-mode/index.js +26 -13
- package/dist/agent-core/tools/code-mode/quickjs-engine.js +2 -0
- package/dist/agent-core/tools/contracts.d.ts +6 -0
- package/dist/agent-core/tools/execution.js +7 -0
- package/dist/agent-core/tools/output-projection.js +5 -0
- package/dist/agent-core/tools/permissions.d.ts +85 -0
- package/dist/agent-core/tools/permissions.js +237 -0
- package/dist/agent-core/tools/runtime-context.d.ts +2 -0
- package/dist/cli/main.js +13 -2
- package/dist/index.d.ts +1 -0
- 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?.({
|
|
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
|
|
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
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
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(
|
|
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';
|