@capekai/core 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -0
- package/package.json +105 -0
- package/src/adapters/ai-sdk.ts +84 -0
- package/src/compaction/contracts.ts +82 -0
- package/src/compaction/executor.ts +161 -0
- package/src/compaction/policy.ts +318 -0
- package/src/compaction/recovery.ts +139 -0
- package/src/compaction/task.ts +540 -0
- package/src/configuration/contracts.ts +58 -0
- package/src/configuration/defaults.ts +27 -0
- package/src/configuration/runtime.ts +42 -0
- package/src/configuration/single-model.ts +75 -0
- package/src/context/assembler.ts +112 -0
- package/src/context/index.ts +2 -0
- package/src/context/sources.ts +119 -0
- package/src/context/workspace.ts +63 -0
- package/src/core/agent.ts +401 -0
- package/src/core/build-tools.ts +139 -0
- package/src/core/chat-handler.ts +858 -0
- package/src/core/error-handling.ts +18 -0
- package/src/core/fork.ts +103 -0
- package/src/core/interrupt.ts +192 -0
- package/src/core/message-utils.ts +261 -0
- package/src/core/model-utils.ts +149 -0
- package/src/core/part-utils.ts +88 -0
- package/src/core/provider-utils.ts +67 -0
- package/src/core/revert.ts +46 -0
- package/src/core/step-handlers.ts +157 -0
- package/src/core/stream/finalization.ts +65 -0
- package/src/core/stream/stream-config.ts +82 -0
- package/src/core/stream-handlers.ts +242 -0
- package/src/core/structured-output.ts +68 -0
- package/src/core/tool-builders/agent-tools.ts +71 -0
- package/src/core/tool-builders/external-tools.ts +179 -0
- package/src/core/tool-builders/types.ts +16 -0
- package/src/core/tool-builders/workspace-tools.ts +293 -0
- package/src/core/tool-capabilities.ts +65 -0
- package/src/goals/evaluator.ts +171 -0
- package/src/goals/index.ts +3 -0
- package/src/goals/loop.ts +167 -0
- package/src/goals/service.ts +39 -0
- package/src/index.ts +10 -0
- package/src/internal/ask-authority.ts +29 -0
- package/src/internal/composition.ts +44 -0
- package/src/internal/configuration.ts +22 -0
- package/src/internal/execution.ts +108 -0
- package/src/internal/hosts.ts +64 -0
- package/src/internal/plugins.ts +71 -0
- package/src/internal/providers.ts +32 -0
- package/src/internal/sandbox.ts +19 -0
- package/src/internal/tools.ts +48 -0
- package/src/internal/workspace.ts +25 -0
- package/src/kernel/diagnostics.ts +249 -0
- package/src/kernel/errors.ts +120 -0
- package/src/kernel/events.ts +82 -0
- package/src/kernel/index.ts +72 -0
- package/src/kernel/kernel.ts +62 -0
- package/src/kernel/lifecycle.ts +72 -0
- package/src/kernel/plugin.ts +218 -0
- package/src/kernel/registry.ts +493 -0
- package/src/kernel/scope.ts +776 -0
- package/src/kernel/service-key.ts +19 -0
- package/src/kernel/types.ts +317 -0
- package/src/memory/index.ts +2 -0
- package/src/memory/memory-tool.ts +75 -0
- package/src/memory/registry.ts +172 -0
- package/src/permission/ask-user-api.ts +70 -0
- package/src/permission/contracts.ts +135 -0
- package/src/permission/permission-request-manager.ts +58 -0
- package/src/permission/policy.ts +277 -0
- package/src/permission/runtime.ts +612 -0
- package/src/plugins/compaction-policy.ts +46 -0
- package/src/plugins/compose.ts +171 -0
- package/src/plugins/context-sections.ts +246 -0
- package/src/plugins/default-agent-driver.ts +14 -0
- package/src/plugins/facade-plugins.ts +129 -0
- package/src/plugins/goal-domain.ts +82 -0
- package/src/plugins/legacy-system-message.ts +152 -0
- package/src/plugins/loaded-tools.ts +23 -0
- package/src/plugins/memory-domain.ts +264 -0
- package/src/plugins/orchestrator-session.ts +29 -0
- package/src/plugins/permission-policy.ts +49 -0
- package/src/plugins/retry-policy.ts +28 -0
- package/src/plugins/scheduler-domain.ts +192 -0
- package/src/plugins/service-keys.ts +294 -0
- package/src/plugins/session-search-domain.ts +238 -0
- package/src/plugins/skills-domain.ts +272 -0
- package/src/plugins/subagent-domain.ts +287 -0
- package/src/plugins/tool-catalog.ts +78 -0
- package/src/plugins/tool-output-policy.ts +52 -0
- package/src/plugins/value-plugins.ts +150 -0
- package/src/plugins/workflow-domain.ts +198 -0
- package/src/plugins/workspace-policy.ts +37 -0
- package/src/providers/registry.ts +63 -0
- package/src/providers/types.ts +44 -0
- package/src/retry/policy.ts +282 -0
- package/src/retry/stream-chat.ts +312 -0
- package/src/runtime/agent-runtime.ts +83 -0
- package/src/runtime/default-agent-driver.ts +23 -0
- package/src/runtime/domain-tool-source.ts +156 -0
- package/src/runtime/events.ts +61 -0
- package/src/runtime/host-dependencies.ts +71 -0
- package/src/runtime/host-guidance.ts +22 -0
- package/src/runtime/host-layout.ts +23 -0
- package/src/runtime/host.ts +129 -0
- package/src/runtime/standalone-host.ts +118 -0
- package/src/sandbox/controller.ts +204 -0
- package/src/sandbox/model.ts +305 -0
- package/src/sandbox/provider.ts +53 -0
- package/src/sandbox/types.ts +110 -0
- package/src/scheduler/host.ts +22 -0
- package/src/scheduler/scheduler-tool.ts +172 -0
- package/src/session-search/host.ts +56 -0
- package/src/session-search/index.ts +23 -0
- package/src/session-search/session-search-tool.ts +151 -0
- package/src/skills/index.ts +3 -0
- package/src/skills/registry.ts +63 -0
- package/src/skills/skill-manage-tool.ts +205 -0
- package/src/skills/skill-tool.ts +42 -0
- package/src/storage/contracts.ts +159 -0
- package/src/storage/memory.ts +321 -0
- package/src/storage/options.ts +75 -0
- package/src/storage/runtime.ts +115 -0
- package/src/storage/sqlite-tool-output-artifacts.ts +106 -0
- package/src/storage/sqlite.ts +321 -0
- package/src/storage/tool-output-artifacts.ts +75 -0
- package/src/storage.ts +31 -0
- package/src/subagent/child-session.ts +282 -0
- package/src/subagent/guidance.ts +8 -0
- package/src/subagent/policy.ts +198 -0
- package/src/subagent/task-tool.ts +584 -0
- package/src/tool-output/contracts.ts +111 -0
- package/src/tool-output/policy.ts +410 -0
- package/src/tool.ts +1 -0
- package/src/tools/executor.ts +258 -0
- package/src/tools/install-manifest.ts +40 -0
- package/src/tools/llm-api.ts +77 -0
- package/src/tools/registry.ts +206 -0
- package/src/tools/tool-artifact.ts +182 -0
- package/src/tools/tool-source.ts +53 -0
- package/src/utils/errors.ts +334 -0
- package/src/utils/strip-visualization.ts +50 -0
- package/src/workflow/decomposer.ts +139 -0
- package/src/workflow/execution.ts +523 -0
- package/src/workflow/orchestrator-session.ts +161 -0
- package/src/workflow/synthesizer.ts +130 -0
- package/src/workspace/contracts.ts +135 -0
- package/src/workspace/policy.ts +327 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* C6 permission policy contracts.
|
|
3
|
+
*
|
|
4
|
+
* The agent-scoped permission surface is split into two contracts:
|
|
5
|
+
*
|
|
6
|
+
* - `AskPermissionPolicyService` (REPLACEABLE advice/config): frozen
|
|
7
|
+
* timeouts, authority and value extraction, the response validators, the
|
|
8
|
+
* auto-approval bound, permission-key derivation, and dangerous-shell
|
|
9
|
+
* detection. A custom provider may replace all of these, but its advice
|
|
10
|
+
* can never change a mandatory decision: the runtime owns denial and
|
|
11
|
+
* grant construction.
|
|
12
|
+
* - `PermissionRuntimeService` (NON-REPLACEABLE lifecycle): request-id
|
|
13
|
+
* routing, pending-ask and waiter registries, timeouts and cleanup,
|
|
14
|
+
* response validation against the module-level validators, raw-audit
|
|
15
|
+
* denial, waiter resolution, canonical grant construction and persistence
|
|
16
|
+
* (module-level `buildGrantParams` with the dangerous-shell once
|
|
17
|
+
* downgrade), and the persistence calls through the interaction host.
|
|
18
|
+
*
|
|
19
|
+
* The Jean2 adapters keep persistence, WebSocket delivery, notifications,
|
|
20
|
+
* and controller authority: every host-facing operation goes through the
|
|
21
|
+
* `InteractionHost` installed by the compatibility bindings.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import type { Ask, AskApi, AskPermissionResponse } from '@capekai/tool';
|
|
25
|
+
import type { PermissionRiskLevel } from '@capekai/tool';
|
|
26
|
+
import type { AskAuthority, AskRequestMessage, AskTimedOutMessage, ClientCapability } from '@capekai/types';
|
|
27
|
+
import type { PendingAskRecord } from '../runtime/host';
|
|
28
|
+
|
|
29
|
+
/** Provider options translated at composition. The generic ask timeout has
|
|
30
|
+
* no current configuration source and stays the fixed 5-minute constant;
|
|
31
|
+
* the permission timeout translates from the composed runtime host's
|
|
32
|
+
* `getPermissionTimeoutMs()`. */
|
|
33
|
+
export interface AskPermissionPolicyOptions {
|
|
34
|
+
askTimeoutMs: number;
|
|
35
|
+
permissionTimeoutMs: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface RequestPermissionParams {
|
|
39
|
+
sessionId: string;
|
|
40
|
+
rootSessionId?: string;
|
|
41
|
+
workspaceId?: string;
|
|
42
|
+
toolCallId: string;
|
|
43
|
+
toolName: string;
|
|
44
|
+
ask: Ask;
|
|
45
|
+
broadcastFn: PermissionBroadcastFn;
|
|
46
|
+
timeoutMs?: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type AskBroadcastFn = (message: AskRequestMessage | AskTimedOutMessage) => void;
|
|
50
|
+
export type PermissionBroadcastFn = (message: AskRequestMessage | AskTimedOutMessage) => void;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Replaceable permission advice and configuration. Every method is pure
|
|
54
|
+
* advice over the injected interaction host; the runtime never trusts these
|
|
55
|
+
* methods for mandatory denial or grant decisions.
|
|
56
|
+
*/
|
|
57
|
+
export interface AskPermissionPolicyService {
|
|
58
|
+
readonly id: string;
|
|
59
|
+
/** Frozen provider options. */
|
|
60
|
+
readonly options: Readonly<AskPermissionPolicyOptions>;
|
|
61
|
+
readonly askTimeoutMs: number;
|
|
62
|
+
readonly permissionTimeoutMs: number;
|
|
63
|
+
/** Authority for a generic ask: capability asks targeting the client
|
|
64
|
+
* resolve to global/first_eligible with the required capability; every
|
|
65
|
+
* other ask keeps the controller-only default. */
|
|
66
|
+
resolveAskAuthority(request: Ask): AskAuthority;
|
|
67
|
+
/** Extracts the resolved value from a shaped generic response. */
|
|
68
|
+
extractResolutionValue(response: unknown): unknown;
|
|
69
|
+
/** A permission response is only valid when it is shaped
|
|
70
|
+
* `{ type: 'permission', grant: 'once' | 'session' | 'workspace' | 'deny' }`. */
|
|
71
|
+
isValidPermissionResponse(response: unknown): response is AskPermissionResponse;
|
|
72
|
+
/** True for a valid non-deny permission response. */
|
|
73
|
+
isPermissionApproved(response: unknown): boolean;
|
|
74
|
+
/** Risk ordering check used for auto-approval bounds. */
|
|
75
|
+
isRiskAtOrBelow(risk: PermissionRiskLevel, max: PermissionRiskLevel): boolean;
|
|
76
|
+
/** Server auto-approval advice: true when the session severity is above
|
|
77
|
+
* off/undefined and the ask's risk is within it. */
|
|
78
|
+
shouldAutoApprove(sessionId: string, ask: Ask): Promise<boolean>;
|
|
79
|
+
/** Permission key precedence: first pattern, then resource, then tool name. */
|
|
80
|
+
buildPermissionKey(
|
|
81
|
+
toolName: string,
|
|
82
|
+
resource: string | undefined,
|
|
83
|
+
patterns: string[] | undefined,
|
|
84
|
+
): string;
|
|
85
|
+
/** Dangerous shell identity detection advice. */
|
|
86
|
+
isDangerousShellIdentity(identity: string | undefined): boolean;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Non-replaceable permission runtime and lifecycle. Owns the pending-ask
|
|
91
|
+
* and waiter registries, request-id routing, timeout/cleanup, response
|
|
92
|
+
* validation, raw-audit denial, waiter resolution, and canonical grant
|
|
93
|
+
* construction/persistence.
|
|
94
|
+
*/
|
|
95
|
+
export interface PermissionRuntimeService {
|
|
96
|
+
readonly id: string;
|
|
97
|
+
/** The advice provider currently active for this runtime (the scoped
|
|
98
|
+
* provider while a scope is entered, otherwise the bound provider). */
|
|
99
|
+
readonly provider: AskPermissionPolicyService;
|
|
100
|
+
|
|
101
|
+
// Generic ask lifecycle (former tools/ask-user-api.ts surface).
|
|
102
|
+
createAskApi(
|
|
103
|
+
sessionId: string,
|
|
104
|
+
toolCallId: string,
|
|
105
|
+
toolName: string,
|
|
106
|
+
broadcastFn: AskBroadcastFn,
|
|
107
|
+
workspaceId?: string,
|
|
108
|
+
rootSessionId?: string,
|
|
109
|
+
): AskApi;
|
|
110
|
+
resolveAsk(toolCallId: string, response: unknown, requestId?: string): Promise<boolean>;
|
|
111
|
+
rejectAsk(toolCallId: string, error: Error): Promise<boolean>;
|
|
112
|
+
rejectPendingAsksByToolCallId(toolCallId: string, error?: Error): Promise<string[]>;
|
|
113
|
+
rejectPendingAsksBySession(sessionId: string, error?: Error): Promise<string[]>;
|
|
114
|
+
hasPendingAsk(toolCallId: string): boolean;
|
|
115
|
+
getAuthorityForPendingAsk(toolCallId: string): AskAuthority | undefined;
|
|
116
|
+
getSessionIdForPendingAsk(toolCallId: string, requestId?: string): Promise<string | null>;
|
|
117
|
+
listPendingAsksBySession(sessionId: string): Promise<PendingAskRecord[]>;
|
|
118
|
+
listPendingAsksByRootSession(rootSessionId: string): Promise<PendingAskRecord[]>;
|
|
119
|
+
|
|
120
|
+
// Permission lifecycle (former tools/permission-request-manager.ts surface).
|
|
121
|
+
requestPermission(params: RequestPermissionParams): Promise<unknown>;
|
|
122
|
+
resolvePermission(requestId: string, response: unknown): Promise<boolean>;
|
|
123
|
+
rejectPermission(requestId: string, error: Error): boolean;
|
|
124
|
+
rejectPermissionsByToolCallId(toolCallId: string, error?: Error): Promise<string[]>;
|
|
125
|
+
rejectPermissionsBySession(sessionId: string, error?: Error): Promise<string[]>;
|
|
126
|
+
getPendingRequestsByRootSession(rootSessionId: string): Promise<PendingAskRecord[]>;
|
|
127
|
+
expireOldRequests(maxAgeMs: number): Promise<number>;
|
|
128
|
+
hasPendingWaiter(requestId: string): boolean;
|
|
129
|
+
getPendingWaiterCount(): number;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Capability-ask authority carrying the required client capability. */
|
|
133
|
+
export type CapabilityAskAuthority = AskAuthority & {
|
|
134
|
+
requiredCapabilities?: ClientCapability[];
|
|
135
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* C6 permission lifecycle surface over the NON-REPLACEABLE permission
|
|
3
|
+
* runtime. These exports preserve the exact pre-C6
|
|
4
|
+
* `tools/permission-request-manager.ts` identities; the waiters, timers,
|
|
5
|
+
* grant reuse, validation, and audit decisions resolve through
|
|
6
|
+
* `getPermissionRuntimeService()`.
|
|
7
|
+
*
|
|
8
|
+
* The runtime enforces the mandatory deny invariant from the module-level
|
|
9
|
+
* `isValidPermissionResponse` (never from provider advice), records the raw
|
|
10
|
+
* payload in the denied audit record (preserving the pre-C6 audit
|
|
11
|
+
* behavior), and persists only the canonical `buildGrantParams` output, so
|
|
12
|
+
* a replaced advice provider can never approve malformed responses or
|
|
13
|
+
* create grants outside the canonical allowed scopes.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { PendingAskRecord } from '../runtime/host';
|
|
17
|
+
import { getPermissionRuntimeService, } from './runtime';
|
|
18
|
+
import { PERMISSION_TIMEOUT } from './policy';
|
|
19
|
+
import type { PermissionBroadcastFn, RequestPermissionParams } from './contracts';
|
|
20
|
+
|
|
21
|
+
export { PERMISSION_TIMEOUT };
|
|
22
|
+
export type { PermissionBroadcastFn, RequestPermissionParams };
|
|
23
|
+
|
|
24
|
+
export function requestPermission(params: RequestPermissionParams): Promise<unknown> {
|
|
25
|
+
return getPermissionRuntimeService().requestPermission(params);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function resolvePermission(requestId: string, response: unknown): Promise<boolean> {
|
|
29
|
+
return getPermissionRuntimeService().resolvePermission(requestId, response);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function rejectPermission(requestId: string, error: Error): boolean {
|
|
33
|
+
return getPermissionRuntimeService().rejectPermission(requestId, error);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function rejectPermissionsByToolCallId(toolCallId: string, error?: Error): Promise<string[]> {
|
|
37
|
+
return getPermissionRuntimeService().rejectPermissionsByToolCallId(toolCallId, error);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function rejectPermissionsBySession(sessionId: string, error?: Error): Promise<string[]> {
|
|
41
|
+
return getPermissionRuntimeService().rejectPermissionsBySession(sessionId, error);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function getPendingRequestsByRootSession(rootSessionId: string): Promise<PendingAskRecord[]> {
|
|
45
|
+
return getPermissionRuntimeService().getPendingRequestsByRootSession(rootSessionId);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function expireOldRequests(maxAgeMs: number): Promise<number> {
|
|
49
|
+
return getPermissionRuntimeService().expireOldRequests(maxAgeMs);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function hasPendingWaiter(requestId: string): boolean {
|
|
53
|
+
return getPermissionRuntimeService().hasPendingWaiter(requestId);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function getPendingWaiterCount(): number {
|
|
57
|
+
return getPermissionRuntimeService().getPendingWaiterCount();
|
|
58
|
+
}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* C6 permission policy: REPLACEABLE advice and configuration only.
|
|
3
|
+
*
|
|
4
|
+
* The non-replaceable lifecycle (waiters, routing, validation enforcement,
|
|
5
|
+
* raw-audit denial, canonical grant construction and persistence) lives in
|
|
6
|
+
* `permission/runtime.ts`. This module owns:
|
|
7
|
+
*
|
|
8
|
+
* - The frozen provider options (generic ask timeout constant and the
|
|
9
|
+
* composition-translated permission timeout).
|
|
10
|
+
* - Authority resolution and value extraction for generic asks.
|
|
11
|
+
* - The module-level validators and canonical grant builder that BOTH the
|
|
12
|
+
* default provider and the runtime close over. A custom provider can
|
|
13
|
+
* override the advice methods on its instance, but the runtime never
|
|
14
|
+
* consults those overrides for mandatory decisions.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
18
|
+
import type { Ask } from '@capekai/tool';
|
|
19
|
+
import type {
|
|
20
|
+
AskAuthority, AskPermissionResponse, ClientCapability, GrantScope, PermissionIntent,
|
|
21
|
+
} from '@capekai/types';
|
|
22
|
+
import type { PermissionAsk, PermissionRiskLevel } from '@capekai/tool';
|
|
23
|
+
import { SHELL_FILESYSTEM_COMMANDS } from '@capekai/tool';
|
|
24
|
+
import { SHELL_DANGEROUS_COMMANDS } from '@capekai/tool';
|
|
25
|
+
import { getRuntimeHost } from '../runtime/host';
|
|
26
|
+
import type {
|
|
27
|
+
CreateGrantParams,
|
|
28
|
+
PendingAskRecord,
|
|
29
|
+
} from '../runtime/host';
|
|
30
|
+
import type {
|
|
31
|
+
AskPermissionPolicyOptions,
|
|
32
|
+
AskPermissionPolicyService,
|
|
33
|
+
} from './contracts';
|
|
34
|
+
|
|
35
|
+
export const ASK_TIMEOUT = 5 * 60 * 1000;
|
|
36
|
+
|
|
37
|
+
/** Legacy export from the pre-C6 permission-request-manager module. */
|
|
38
|
+
export const PERMISSION_TIMEOUT = 30 * 60 * 1000;
|
|
39
|
+
|
|
40
|
+
const DEFAULT_ASK_AUTHORITY: AskAuthority = {
|
|
41
|
+
visibilityScope: 'controller_only',
|
|
42
|
+
resolutionMode: 'controller_only',
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const RISK_ORDER: PermissionRiskLevel[] = ['none', 'low', 'medium', 'high', 'critical'];
|
|
46
|
+
const VALID_GRANT_SCOPES: GrantScope[] = ['once', 'session', 'workspace'];
|
|
47
|
+
|
|
48
|
+
export interface AskPermissionServiceCreateOptions {
|
|
49
|
+
id?: string;
|
|
50
|
+
/** Frozen composition-time options. When omitted (the process-default
|
|
51
|
+
* fallback), the permission timeout resolves live from the installed
|
|
52
|
+
* interaction host exactly like the pre-C6 module code. */
|
|
53
|
+
options?: AskPermissionPolicyOptions;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function interaction() {
|
|
57
|
+
return getRuntimeHost().interaction;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function resolveOptionsLive(): AskPermissionPolicyOptions {
|
|
61
|
+
return {
|
|
62
|
+
askTimeoutMs: ASK_TIMEOUT,
|
|
63
|
+
permissionTimeoutMs: interaction().getPermissionTimeoutMs(),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ── Module-level non-overridable validators and canonical grants ─────────
|
|
68
|
+
|
|
69
|
+
export function isRiskAtOrBelow(risk: PermissionRiskLevel, max: PermissionRiskLevel): boolean {
|
|
70
|
+
return RISK_ORDER.indexOf(risk) <= RISK_ORDER.indexOf(max);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function isValidPermissionResponse(response: unknown): response is AskPermissionResponse {
|
|
74
|
+
if (!response || typeof response !== 'object') return false;
|
|
75
|
+
const record = response as Record<string, unknown>;
|
|
76
|
+
return record.type === 'permission' && VALID_GRANT_SCOPES.includes(record.grant as GrantScope);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function isPermissionApproved(response: unknown): boolean {
|
|
80
|
+
return isValidPermissionResponse(response) && response.grant !== 'deny';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function buildPermissionKey(
|
|
84
|
+
toolName: string,
|
|
85
|
+
resource: string | undefined,
|
|
86
|
+
patterns: string[] | undefined,
|
|
87
|
+
): string {
|
|
88
|
+
if (patterns && patterns.length > 0) return patterns[0];
|
|
89
|
+
if (resource) return resource;
|
|
90
|
+
return toolName;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function isDangerousShellIdentity(identity: string | undefined): boolean {
|
|
94
|
+
if (!identity) return false;
|
|
95
|
+
const lower = identity.toLowerCase();
|
|
96
|
+
const dangerous = SHELL_DANGEROUS_COMMANDS.some(
|
|
97
|
+
command => lower === command || lower.startsWith(command + ' '),
|
|
98
|
+
);
|
|
99
|
+
if (dangerous) return true;
|
|
100
|
+
return SHELL_FILESYSTEM_COMMANDS.some(
|
|
101
|
+
command => lower === command || lower.startsWith(command + ' '),
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Canonical grant construction: the only grant-shape source the runtime
|
|
106
|
+
* persists. Enforces allowed scopes, the session-duration default, the
|
|
107
|
+
* root binding, and the dangerous-shell once downgrade. */
|
|
108
|
+
export function buildGrantParams(
|
|
109
|
+
record: PendingAskRecord,
|
|
110
|
+
response: AskPermissionResponse,
|
|
111
|
+
): CreateGrantParams[] {
|
|
112
|
+
if (!record.workspaceId || response.grant === 'deny') return [];
|
|
113
|
+
const permAsk = record.ask as PermissionAsk;
|
|
114
|
+
let grantScope: GrantScope = response.grant;
|
|
115
|
+
const boundRootSessionId = record.rootSessionId ?? record.sessionId;
|
|
116
|
+
|
|
117
|
+
if (permAsk.intents && permAsk.intents.length > 0) {
|
|
118
|
+
const intent: PermissionIntent = permAsk.intents[0];
|
|
119
|
+
if (!intent.allowedScopes.includes(grantScope) || grantScope === 'once') return [];
|
|
120
|
+
const duration = response.duration || (grantScope === 'session' ? 30 * 60 * 1000 : undefined);
|
|
121
|
+
|
|
122
|
+
return intent.targets.map((target) => ({
|
|
123
|
+
workspaceId: record.workspaceId!,
|
|
124
|
+
toolName: record.toolName,
|
|
125
|
+
resource: intent.resource,
|
|
126
|
+
action: intent.action,
|
|
127
|
+
permissionKey: target.target,
|
|
128
|
+
grantOptions: {
|
|
129
|
+
scope: grantScope,
|
|
130
|
+
matcher: target.matcher === 'prefix' ? 'prefix' : 'exact',
|
|
131
|
+
patterns: [target.target],
|
|
132
|
+
action: intent.action,
|
|
133
|
+
duration: grantScope === 'session' ? duration : undefined,
|
|
134
|
+
description: permAsk.question,
|
|
135
|
+
boundRootSessionId: grantScope === 'session' ? boundRootSessionId : undefined,
|
|
136
|
+
},
|
|
137
|
+
}));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const metadata = permAsk.metadata as Record<string, unknown> | undefined;
|
|
141
|
+
const identity = typeof metadata?.baseCommand === 'string' ? metadata.baseCommand : undefined;
|
|
142
|
+
if (permAsk.resource === 'shell-command' && isDangerousShellIdentity(identity)) {
|
|
143
|
+
grantScope = 'once';
|
|
144
|
+
}
|
|
145
|
+
const duration = response.duration || (grantScope === 'session' ? 30 * 60 * 1000 : undefined);
|
|
146
|
+
return [{
|
|
147
|
+
workspaceId: record.workspaceId,
|
|
148
|
+
toolName: record.toolName,
|
|
149
|
+
resource: permAsk.resource ?? 'file',
|
|
150
|
+
action: permAsk.action,
|
|
151
|
+
permissionKey: buildPermissionKey(record.toolName, permAsk.resource ?? 'file', permAsk.patterns),
|
|
152
|
+
grantOptions: {
|
|
153
|
+
scope: grantScope,
|
|
154
|
+
matcher: (permAsk.resource ?? 'file') === 'shell-command' ? 'shell-command' : 'exact',
|
|
155
|
+
patterns: permAsk.patterns,
|
|
156
|
+
action: permAsk.action,
|
|
157
|
+
duration: grantScope === 'session' ? duration : undefined,
|
|
158
|
+
description: permAsk.question,
|
|
159
|
+
boundRootSessionId: grantScope === 'session' ? boundRootSessionId : undefined,
|
|
160
|
+
},
|
|
161
|
+
}];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function resolveAskAuthority(request: Ask): AskAuthority {
|
|
165
|
+
if (
|
|
166
|
+
request.type === 'client_capability'
|
|
167
|
+
&& 'target' in request
|
|
168
|
+
&& request.target === 'client'
|
|
169
|
+
) {
|
|
170
|
+
return {
|
|
171
|
+
visibilityScope: 'global',
|
|
172
|
+
resolutionMode: 'first_eligible',
|
|
173
|
+
requiredCapabilities: [request.capability as ClientCapability],
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
return DEFAULT_ASK_AUTHORITY;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function extractResolutionValue(response: unknown): unknown {
|
|
180
|
+
if (!response || typeof response !== 'object') return response;
|
|
181
|
+
const record = response as Record<string, unknown>;
|
|
182
|
+
|
|
183
|
+
switch (record.type) {
|
|
184
|
+
case 'single_select':
|
|
185
|
+
return record.value;
|
|
186
|
+
case 'multi_select':
|
|
187
|
+
return record.values;
|
|
188
|
+
case 'text':
|
|
189
|
+
return record.value;
|
|
190
|
+
case 'confirm':
|
|
191
|
+
return record.confirmed;
|
|
192
|
+
case 'form':
|
|
193
|
+
return response;
|
|
194
|
+
case 'client_capability':
|
|
195
|
+
return record.result;
|
|
196
|
+
default:
|
|
197
|
+
return response;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** The C6 default advice provider wrapping the exact pre-C6 behavior. */
|
|
202
|
+
export function createAskPermissionService(
|
|
203
|
+
createOptions: AskPermissionServiceCreateOptions = {},
|
|
204
|
+
): AskPermissionPolicyService {
|
|
205
|
+
const id = createOptions.id ?? 'permission.default';
|
|
206
|
+
const frozenOptions = createOptions.options;
|
|
207
|
+
|
|
208
|
+
function options(): AskPermissionPolicyOptions {
|
|
209
|
+
return frozenOptions ?? resolveOptionsLive();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
id,
|
|
214
|
+
get options(): Readonly<AskPermissionPolicyOptions> {
|
|
215
|
+
return options();
|
|
216
|
+
},
|
|
217
|
+
get askTimeoutMs(): number {
|
|
218
|
+
return options().askTimeoutMs;
|
|
219
|
+
},
|
|
220
|
+
get permissionTimeoutMs(): number {
|
|
221
|
+
return options().permissionTimeoutMs;
|
|
222
|
+
},
|
|
223
|
+
resolveAskAuthority,
|
|
224
|
+
extractResolutionValue,
|
|
225
|
+
isValidPermissionResponse,
|
|
226
|
+
isPermissionApproved,
|
|
227
|
+
isRiskAtOrBelow,
|
|
228
|
+
async shouldAutoApprove(sessionId: string, ask: Ask): Promise<boolean> {
|
|
229
|
+
if (ask.type !== 'permission') return false;
|
|
230
|
+
const risk = (ask as PermissionAsk).risk;
|
|
231
|
+
if (!risk) return false;
|
|
232
|
+
|
|
233
|
+
const maxSeverity = await interaction().getSessionAutoApproveSeverity(sessionId);
|
|
234
|
+
if (!maxSeverity || maxSeverity === 'off') return false;
|
|
235
|
+
if (!isRiskAtOrBelow(risk, maxSeverity)) return false;
|
|
236
|
+
|
|
237
|
+
console.log(
|
|
238
|
+
`[permissions] Server auto-approve: risk=${risk} maxSeverity=${maxSeverity} session=${sessionId}`,
|
|
239
|
+
);
|
|
240
|
+
return true;
|
|
241
|
+
},
|
|
242
|
+
buildPermissionKey,
|
|
243
|
+
isDangerousShellIdentity,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const scopedPolicy = new AsyncLocalStorage<AskPermissionPolicyService>();
|
|
248
|
+
let processDefaultPolicy: AskPermissionPolicyService | undefined;
|
|
249
|
+
|
|
250
|
+
/** Resolves the service seeded for the active agent scope, falling back to
|
|
251
|
+
* one lazily created process-default service for consumers that run outside
|
|
252
|
+
* a composed scope (the current Jean2 server path). */
|
|
253
|
+
export function getAskPermissionPolicy(): AskPermissionPolicyService {
|
|
254
|
+
return scopedPolicy.getStore()
|
|
255
|
+
?? (processDefaultPolicy ??= createAskPermissionService({ id: 'permission.process-default' }));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** The scoped policy only (undefined outside a composed scope). Used by the
|
|
259
|
+
* runtime to prefer the actively seeded advice provider. */
|
|
260
|
+
export function getActiveAskPermissionPolicy(): AskPermissionPolicyService | undefined {
|
|
261
|
+
return scopedPolicy.getStore();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** Seeds a service for the callback duration. `enterAgentScope` seeds the
|
|
265
|
+
* composed agent scope's service here. */
|
|
266
|
+
export function withAskPermissionPolicy<T>(
|
|
267
|
+
service: AskPermissionPolicyService,
|
|
268
|
+
callback: () => T,
|
|
269
|
+
): T {
|
|
270
|
+
return scopedPolicy.run(service, callback);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** Test-only reset of the lazily created process default. Exported from this
|
|
274
|
+
* module only; no package subpath re-exports it. */
|
|
275
|
+
export function resetDefaultAskPermissionPolicyForTests(): void {
|
|
276
|
+
processDefaultPolicy = undefined;
|
|
277
|
+
}
|