@logicseek/hello-cc 1.0.5 → 1.1.0-rc.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +72 -0
- package/docs/codex-account.zh-CN.md +38 -0
- package/docs/codex-protocol.md +59 -0
- package/docs/commands.md +9 -0
- package/docs/commands.zh-CN.md +7 -0
- package/docs/macos-process-identity.md +2 -0
- package/docs/native.md +179 -14
- package/docs/native.zh-CN.md +136 -6
- package/docs/web-handoff.zh-CN.md +118 -8
- package/lib/cli/app.mjs +47 -4
- package/lib/cli/commands/coordination.mjs +14 -4
- package/lib/cli/commands/dsh.mjs +1 -1
- package/lib/cli/commands/gc.mjs +22 -15
- package/lib/cli/commands/hook.mjs +19 -3
- package/lib/cli/commands/install.mjs +80 -10
- package/lib/cli/commands/misc.mjs +36 -9
- package/lib/cli/commands/native.mjs +4 -38
- package/lib/cli/commands/peer.mjs +3 -0
- package/lib/cli/commands/run.mjs +13 -6
- package/lib/cli/commands/tmux.mjs +1 -2
- package/lib/cli-runtime.mjs +19 -2
- package/lib/core/coordination/results.mjs +27 -10
- package/lib/core/sessions/hook-root-identity.mjs +47 -0
- package/lib/core/sessions/launch.mjs +7 -1
- package/lib/db/connection.mjs +140 -42
- package/lib/db/schema.mjs +59 -0
- package/lib/diagnostics/codex-protocol.mjs +105 -0
- package/lib/guidance.mjs +21 -36
- package/lib/integrations/codex-account.mjs +143 -0
- package/lib/integrations/codex-interactions.mjs +13 -11
- package/lib/integrations/dsh-collaboration.mjs +6 -12
- package/lib/integrations/dsh-cordis.mjs +17 -13
- package/lib/integrations/dsh.mjs +16 -11
- package/lib/integrations/mcp-elicitation.mjs +116 -0
- package/lib/integrations/mcp-url-elicitation.mjs +25 -0
- package/lib/integrations/native/claude.mjs +24 -6
- package/lib/integrations/native/codex.mjs +38 -7
- package/lib/integrations/native/dsh-acp.mjs +67 -24
- package/lib/integrations/native/index.mjs +4 -1
- package/lib/integrations/native/interactions.mjs +2 -1
- package/lib/integrations/native/jsonrpc.mjs +37 -5
- package/lib/integrations/native/telemetry.mjs +30 -0
- package/lib/process/identity.mjs +4 -0
- package/lib/process/pinned-cwd.mjs +302 -0
- package/lib/process/pinned-guidance.mjs +138 -0
- package/lib/process/pinned-project-state.mjs +105 -0
- package/lib/process/selected-cwd-identity.mjs +95 -0
- package/lib/runtime/buffer-evidence.mjs +2 -1
- package/lib/runtime/client.mjs +9 -0
- package/lib/runtime/native/client.mjs +4 -1
- package/lib/runtime/native/history.mjs +140 -0
- package/lib/runtime/native/launcher.mjs +106 -0
- package/lib/runtime/native/service.mjs +64 -8
- package/lib/runtime/native/store.mjs +83 -58
- package/lib/runtime/paths.mjs +17 -2
- package/lib/runtime/private-file.mjs +76 -0
- package/lib/runtime/private-state.mjs +546 -0
- package/lib/runtime/project-context.mjs +2 -1
- package/lib/runtime/project-path.mjs +92 -20
- package/lib/runtime/project-trust.mjs +79 -0
- package/lib/runtime/projects.mjs +9 -3
- package/lib/runtime/state-migration.mjs +697 -0
- package/lib/runtime/state.mjs +69 -27
- package/lib/shared/automatic-result-key.mjs +6 -0
- package/lib/shared/json-file.mjs +55 -6
- package/lib/shared/redact.mjs +2 -1
- package/lib/ui/help.mjs +6 -4
- package/lib/web/agent-defaults.mjs +110 -0
- package/lib/web/auto-attach.mjs +3 -2
- package/lib/web/browser/contracts.d.ts +10 -0
- package/lib/web/browser/core.mjs +1997 -0
- package/lib/web/browser/project-data.mjs +158 -0
- package/lib/web/browser/session-sync.mjs +82 -0
- package/lib/web/browser-assets.mjs +19 -0
- package/lib/web/buffer-gc-runtime.mjs +2 -2
- package/lib/web/codex-app-server.mjs +111 -37
- package/lib/web/codex-sessions.mjs +61 -12
- package/lib/web/context-files.mjs +82 -0
- package/lib/web/cookie-auth.mjs +51 -3
- package/lib/web/external-sessions.mjs +76 -9
- package/lib/web/http-routes.mjs +238 -25
- package/lib/web/http.mjs +50 -4
- package/lib/web/liveness-reaper.mjs +2 -2
- package/lib/web/native-sessions.mjs +323 -14
- package/lib/web/project-contexts.mjs +236 -44
- package/lib/web/project-file-write.mjs +249 -0
- package/lib/web/project-files.mjs +268 -0
- package/lib/web/pty-sessions.mjs +11 -3
- package/lib/web/runtime-main.mjs +132 -80
- package/lib/web/session-serialize.mjs +87 -8
- package/lib/web/session-state-sync.mjs +98 -0
- package/lib/web/shutdown-diagnostics.mjs +38 -0
- package/lib/web/shutdown-response.mjs +21 -0
- package/lib/web/shutdown-state.mjs +91 -0
- package/lib/web/startup.mjs +103 -35
- package/lib/web/task-results.mjs +19 -19
- package/lib/web/tls.mjs +84 -33
- package/lib/web/tmux-sessions.mjs +73 -9
- package/lib/web/tmux-stream.mjs +36 -8
- package/lib/web/ui-agent-defaults.mjs +149 -0
- package/lib/web/ui-agent-start.mjs +330 -0
- package/lib/web/ui-codex-account.mjs +81 -0
- package/lib/web/ui-codex.mjs +105 -85
- package/lib/web/ui-command-palette.mjs +21 -5
- package/lib/web/ui-file-writes.mjs +220 -0
- package/lib/web/ui-files.mjs +231 -0
- package/lib/web/ui-handoff.mjs +10 -2
- package/lib/web/ui-history.mjs +175 -105
- package/lib/web/ui-i18n.mjs +280 -8
- package/lib/web/ui-interactions.mjs +107 -6
- package/lib/web/ui-native-timeline.mjs +326 -0
- package/lib/web/ui-native.mjs +93 -43
- package/lib/web/ui-review.mjs +5 -3
- package/lib/web/ui-safe-markdown.mjs +79 -0
- package/lib/web/ui-session-tools.mjs +224 -0
- package/lib/web/ui-template.mjs +29 -1968
- package/lib/web/ui-terminal-find.mjs +5 -3
- package/lib/web/ui-workbench.mjs +5 -3
- package/lib/web/ui-workspace.mjs +3 -0
- package/package.json +8 -2
- package/scripts/benchmark-session-sync.mjs +53 -0
- package/scripts/codex-protocol.mjs +88 -0
- package/scripts/dsh-approval-layers-acceptance.mjs +172 -0
- package/scripts/dsh-package-provenance.mjs +118 -0
- package/scripts/mcp-form-live-fixture.mjs +81 -0
- package/scripts/native-bounded-live-acceptance.mjs +193 -0
- package/scripts/native-fixture-root.mjs +49 -0
- package/scripts/native-installed-acceptance.mjs +611 -0
- package/scripts/native-stability-checks.mjs +40 -0
- package/scripts/regression-cookie-expiry.mjs +70 -0
- package/scripts/regression-http-transport.mjs +9 -0
- package/scripts/regression-terminal-marker.mjs +95 -0
- package/scripts/regression.mjs +481 -219
- package/scripts/shim-reentry-probe.mjs +105 -0
- package/scripts/web-acceptance-diagnostics.mjs +90 -0
- package/scripts/web-codex-account-acceptance.mjs +178 -0
- package/scripts/web-file-preview-checks.mjs +302 -0
- package/scripts/web-mcp-form-acceptance.mjs +134 -0
- package/scripts/web-mcp-form-live-acceptance.mjs +368 -0
- package/scripts/web-native-handoff-checks.mjs +128 -0
- package/scripts/web-native-interaction-acceptance.mjs +219 -18
- package/scripts/web-native-interaction-diagnostics.mjs +80 -0
- package/scripts/web-native-model-acceptance.mjs +22 -7
- package/scripts/web-session-tools-checks.mjs +112 -0
- package/scripts/web-workbench-acceptance.mjs +435 -0
- package/scripts/web-workbench-installed-acceptance.mjs +122 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// Read-only projection of official App Server account and quota data. Never
|
|
2
|
+
// forward emails, credential identifiers, tokens, raw errors or unknown fields.
|
|
3
|
+
const PLANS = new Set(['free', 'go', 'plus', 'pro', 'prolite', 'team',
|
|
4
|
+
'self_serve_business_usage_based', 'business', 'enterprise_cbp_usage_based', 'enterprise', 'edu', 'unknown']);
|
|
5
|
+
const TYPES = new Set(['apiKey', 'chatgpt', 'amazonBedrock']);
|
|
6
|
+
const REASONS = new Set(['rate_limit_reached', 'workspace_owner_credits_depleted', 'workspace_member_credits_depleted',
|
|
7
|
+
'workspace_owner_usage_limit_reached', 'workspace_member_usage_limit_reached']);
|
|
8
|
+
const record = value => value && typeof value === 'object' && !Array.isArray(value);
|
|
9
|
+
const finite = value => typeof value === 'number' && Number.isFinite(value) && value >= 0;
|
|
10
|
+
const integer = value => Number.isSafeInteger(value) && value >= 0;
|
|
11
|
+
const bucketId = value => typeof value === 'string' && /^[A-Za-z0-9_.-]{1,80}$/.test(value) ? value : null;
|
|
12
|
+
|
|
13
|
+
function windowValue(value) {
|
|
14
|
+
if (!record(value) || !finite(value.usedPercent)) return null;
|
|
15
|
+
return { usedPercent: value.usedPercent,
|
|
16
|
+
resetsAt: integer(value.resetsAt) ? value.resetsAt : null,
|
|
17
|
+
windowDurationMins: integer(value.windowDurationMins) && value.windowDurationMins > 0 ? value.windowDurationMins : null };
|
|
18
|
+
}
|
|
19
|
+
function bucketValue(value, id = null) {
|
|
20
|
+
if (!record(value)) return null;
|
|
21
|
+
const primary = windowValue(value.primary), secondary = windowValue(value.secondary);
|
|
22
|
+
const credits = record(value.credits) && typeof value.credits.hasCredits === 'boolean' && typeof value.credits.unlimited === 'boolean'
|
|
23
|
+
? { hasCredits: value.credits.hasCredits, unlimited: value.credits.unlimited } : null;
|
|
24
|
+
if (!primary && !secondary && !credits && !REASONS.has(value.rateLimitReachedType)) return null;
|
|
25
|
+
return { id: bucketId(id) || bucketId(value.limitId), primary, secondary, credits,
|
|
26
|
+
planType: PLANS.has(value.planType) ? value.planType : null,
|
|
27
|
+
reached: REASONS.has(value.rateLimitReachedType) ? value.rateLimitReachedType : null };
|
|
28
|
+
}
|
|
29
|
+
function unavailableReason(error) {
|
|
30
|
+
return error?.code === -32601 || (error?.extra?.rpcCode ?? error?.extra?.rpc_code) === -32601 ? 'unsupported' : 'unavailable';
|
|
31
|
+
}
|
|
32
|
+
function accountValue(value) {
|
|
33
|
+
if (!record(value) || typeof value.requiresOpenaiAuth !== 'boolean' ||
|
|
34
|
+
(value.account != null && (!record(value.account) || !TYPES.has(value.account.type)))) return null;
|
|
35
|
+
const type = value.account?.type || null;
|
|
36
|
+
return { requiresOpenaiAuth: value.requiresOpenaiAuth, type,
|
|
37
|
+
planType: type === 'chatgpt' && PLANS.has(value.account.planType) ? value.account.planType : null,
|
|
38
|
+
authentication: value.requiresOpenaiAuth === false ? 'providerManaged' : type ? 'authenticated' : 'required' };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function createCodexAccountState({ request, onChange = () => {}, now = Date.now } = {}) {
|
|
42
|
+
let state = { status: 'unknown', stale: false, reason: null, type: null, planType: null,
|
|
43
|
+
authentication: 'unknown', requiresOpenaiAuth: null, checkedAt: null,
|
|
44
|
+
rateLimits: { status: 'unknown', stale: false, reason: null, checkedAt: null, buckets: [] } };
|
|
45
|
+
let accountRevision = 0, limitsRevision = 0, inFlight = null, closed = false, recheck = false;
|
|
46
|
+
const snapshot = () => structuredClone(state);
|
|
47
|
+
const publish = () => { try { onChange(snapshot()); } catch {} };
|
|
48
|
+
function relevantLimits() { return state.status === 'ready' && state.type === 'chatgpt' && state.requiresOpenaiAuth === true; }
|
|
49
|
+
function notification(method, params = {}) {
|
|
50
|
+
if (!['account/updated', 'account/rateLimits/updated', 'account/login/completed'].includes(method)) return false;
|
|
51
|
+
if (closed) return true;
|
|
52
|
+
if (method !== 'account/rateLimits/updated') {
|
|
53
|
+
// Account changes can precede a read reply. That older response cannot
|
|
54
|
+
// reattach the previous account's quotas to the current executor.
|
|
55
|
+
accountRevision++; limitsRevision++; recheck = true;
|
|
56
|
+
state = { ...state, status: 'unknown', stale: true, reason: null, type: null, planType: null,
|
|
57
|
+
authentication: 'unknown', requiresOpenaiAuth: null,
|
|
58
|
+
rateLimits: { status: 'unknown', stale: false, reason: null, checkedAt: null, buckets: [] } };
|
|
59
|
+
publish();
|
|
60
|
+
queueMicrotask(() => { if (!closed && !inFlight) void refresh(); });
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
if (!relevantLimits()) return true;
|
|
64
|
+
const next = bucketValue(params.rateLimits);
|
|
65
|
+
if (!next) return true;
|
|
66
|
+
limitsRevision++;
|
|
67
|
+
const buckets = state.rateLimits.buckets.slice();
|
|
68
|
+
if (next.id === null && buckets.length === 1) next.id = buckets[0].id;
|
|
69
|
+
if (next.id === null && buckets.length > 1 && !buckets.some(entry => entry.id === null)) return true;
|
|
70
|
+
const index = buckets.findIndex(entry => entry.id === next.id);
|
|
71
|
+
if (index < 0) { if (buckets.length < 16) buckets.push(next); }
|
|
72
|
+
else {
|
|
73
|
+
const previous = buckets[index];
|
|
74
|
+
// Rolling updates are sparse. Null metadata/windows mean unavailable in
|
|
75
|
+
// this update, not a quota of zero or a request to erase the prior value.
|
|
76
|
+
buckets[index] = Object.fromEntries(Object.keys(next).map(key => [key,
|
|
77
|
+
['primary', 'secondary'].includes(key) && next[key] && previous[key]
|
|
78
|
+
? Object.fromEntries(Object.keys(next[key]).map(field => [field, next[key][field] ?? previous[key][field]]))
|
|
79
|
+
: next[key] ?? previous[key]]));
|
|
80
|
+
}
|
|
81
|
+
state.rateLimits = { status: 'ready', stale: false, reason: null, checkedAt: now(), buckets };
|
|
82
|
+
publish(); return true;
|
|
83
|
+
}
|
|
84
|
+
async function read() {
|
|
85
|
+
const revision = accountRevision;
|
|
86
|
+
recheck = false;
|
|
87
|
+
let value;
|
|
88
|
+
try { value = accountValue(await request('account/read', { refreshToken: false }));
|
|
89
|
+
if (!value) throw Object.assign(new Error('Invalid account response'), { code: 'CODEX_ACCOUNT_PROTOCOL_ERROR' });
|
|
90
|
+
} catch (error) {
|
|
91
|
+
if (!closed && revision === accountRevision) {
|
|
92
|
+
state.status = 'unavailable'; state.stale = state.checkedAt !== null; state.reason = unavailableReason(error);
|
|
93
|
+
state.authentication = 'unknown';
|
|
94
|
+
state.rateLimits = { ...state.rateLimits, status: 'unavailable', stale: state.rateLimits.checkedAt !== null, reason: 'unavailable' };
|
|
95
|
+
publish();
|
|
96
|
+
}
|
|
97
|
+
return snapshot();
|
|
98
|
+
}
|
|
99
|
+
if (closed || revision !== accountRevision) return snapshot();
|
|
100
|
+
state = { ...state, ...value, status: 'ready', stale: false, reason: null, checkedAt: now() };
|
|
101
|
+
if (!relevantLimits()) {
|
|
102
|
+
state.rateLimits = { status: 'notApplicable', stale: false, reason: null, checkedAt: null, buckets: [] };
|
|
103
|
+
publish(); return snapshot();
|
|
104
|
+
}
|
|
105
|
+
publish();
|
|
106
|
+
const quotaRevision = limitsRevision;
|
|
107
|
+
try {
|
|
108
|
+
const response = await request('account/rateLimits/read', {});
|
|
109
|
+
if (!record(response) || !record(response.rateLimits)) throw new Error('Invalid quota response');
|
|
110
|
+
const entries = record(response.rateLimitsByLimitId) ? Object.entries(response.rateLimitsByLimitId)
|
|
111
|
+
.filter(([id]) => bucketId(id)).slice(0, 16).map(([id, bucket]) => bucketValue(bucket, id)).filter(Boolean) : [];
|
|
112
|
+
const legacy = bucketValue(response.rateLimits);
|
|
113
|
+
const buckets = entries.length ? entries : legacy ? [legacy] : [];
|
|
114
|
+
if (!closed && revision === accountRevision && quotaRevision === limitsRevision) {
|
|
115
|
+
state.rateLimits = { status: buckets.length ? 'ready' : 'unavailable', stale: false,
|
|
116
|
+
reason: buckets.length ? null : 'unavailable', checkedAt: buckets.length ? now() : null, buckets };
|
|
117
|
+
publish();
|
|
118
|
+
}
|
|
119
|
+
} catch (error) {
|
|
120
|
+
if (!closed && revision === accountRevision && quotaRevision === limitsRevision) {
|
|
121
|
+
state.rateLimits = { ...state.rateLimits, status: 'unavailable', stale: state.rateLimits.checkedAt !== null, reason: unavailableReason(error) };
|
|
122
|
+
publish();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return snapshot();
|
|
126
|
+
}
|
|
127
|
+
function refresh() {
|
|
128
|
+
if (closed) return Promise.resolve(snapshot());
|
|
129
|
+
if (inFlight) return inFlight;
|
|
130
|
+
inFlight = read().finally(() => {
|
|
131
|
+
inFlight = null;
|
|
132
|
+
if (recheck && !closed) queueMicrotask(() => { if (!inFlight && !closed) void refresh(); });
|
|
133
|
+
});
|
|
134
|
+
return inFlight;
|
|
135
|
+
}
|
|
136
|
+
function close() {
|
|
137
|
+
closed = true; accountRevision++; limitsRevision++;
|
|
138
|
+
state.status = 'unavailable'; state.stale = state.checkedAt !== null; state.reason = 'disconnected';
|
|
139
|
+
state.authentication = 'unknown';
|
|
140
|
+
state.rateLimits = { ...state.rateLimits, status: 'unavailable', stale: state.rateLimits.checkedAt !== null, reason: 'disconnected' };
|
|
141
|
+
}
|
|
142
|
+
return { snapshot, refresh, notification, close };
|
|
143
|
+
}
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { createMcpFormValidator } from './mcp-elicitation.mjs';
|
|
2
|
+
import { createMcpUrlValidator } from './mcp-url-elicitation.mjs';
|
|
3
|
+
|
|
4
|
+
const mcpForms = createMcpFormValidator(), mcpUrls = createMcpUrlValidator();
|
|
1
5
|
// Shared validation for the official App Server command/file/permissions/input
|
|
2
6
|
// server requests. Human responses may narrow a request, never widen it.
|
|
3
7
|
export const CODEX_INTERACTION_METHODS = new Set([
|
|
@@ -37,20 +41,18 @@ function subset(selected, requested) {
|
|
|
37
41
|
fail('Granted permissions must be an exact subset of the requested permissions');
|
|
38
42
|
}
|
|
39
43
|
}
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
// and do not pretend arbitrary form fields or URL authentication were answered.
|
|
44
|
+
// Reply only to the correlated request. URL acceptance has no form content or
|
|
45
|
+
// authentication result; the MCP server owns completion of the external flow.
|
|
43
46
|
function mcpResponse(request, input) {
|
|
44
|
-
if (!['accept', 'decline', 'cancel'].includes(input.decision)) fail('An MCP
|
|
47
|
+
if (!['accept', 'decline', 'cancel'].includes(input.decision)) fail('An MCP decision is required');
|
|
45
48
|
if (input.decision !== 'accept') return { action: input.decision };
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
Object.keys(schema).some(key => !['type', 'properties', 'required', 'additionalProperties'].includes(key))) {
|
|
51
|
-
fail('Only an explicit empty-form MCP tool approval is supported; decline or cancel other elicitations');
|
|
49
|
+
if (request.params?.mode === 'url') {
|
|
50
|
+
mcpUrls.describe(request.params);
|
|
51
|
+
if (input.content !== undefined || input.answers !== undefined || input.permissions !== undefined) fail('MCP URL responses cannot contain form content or permissions');
|
|
52
|
+
return { action: 'accept' };
|
|
52
53
|
}
|
|
53
|
-
|
|
54
|
+
const { fields } = mcpForms.describe(request.params);
|
|
55
|
+
return { action: 'accept', content: mcpForms.validate(request.params, input.content === undefined && !fields.length ? {} : input.content) };
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
function permissionsResponse(request, input) {
|
|
@@ -152,18 +152,12 @@ export function createDshCollaboration({ sessionId, cwd, maxContextChars = 16000
|
|
|
152
152
|
dispose() {
|
|
153
153
|
if (disposed) return;
|
|
154
154
|
retiring = true;
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
disposed = true;
|
|
162
|
-
} catch (error) {
|
|
163
|
-
// A lost generation cannot be retried; storage failures can be.
|
|
164
|
-
if (error?.code === 'DSH_COLLABORATION_CONFLICT') disposed = true;
|
|
165
|
-
throw error;
|
|
166
|
-
}
|
|
155
|
+
owned(db => {
|
|
156
|
+
peers.touchPeer(db, peer, 'exited');
|
|
157
|
+
db.prepare('UPDATE peer_bindings SET runtime_target = NULL, updated_at = ? WHERE peer = ? AND runtime_target = ?').run(now(), peer, owner);
|
|
158
|
+
addEvent(db, 'dsh.cordis.closed', peer, null, { session_id: sessionId });
|
|
159
|
+
}, true, true);
|
|
160
|
+
disposed = true;
|
|
167
161
|
}
|
|
168
162
|
};
|
|
169
163
|
}
|
|
@@ -10,8 +10,20 @@ export const name = 'hello-cc-dsh-collaboration';
|
|
|
10
10
|
export const inject = ['agents', 'tools', 'sessions'];
|
|
11
11
|
export const DSH_CORDIS_VERSION = '0.2.0-rc.2';
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
// A failed retirement is retried only after exact Agent disposal, or by that
|
|
14
|
+
// same Agent after its previous plugin context has been disposed.
|
|
15
|
+
const retirementRegistryKey = Symbol.for('@logicseek/hello-cc/dsh-cordis/retirements/v1');
|
|
16
|
+
if (!Object.hasOwn(globalThis, retirementRegistryKey)) {
|
|
17
|
+
Object.defineProperty(globalThis, retirementRegistryKey, {
|
|
18
|
+
value: Object.freeze({ failedRetirements: new Map(), disposedAgents: new WeakSet() })
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
const retirementRegistry = globalThis[retirementRegistryKey];
|
|
22
|
+
if (!(retirementRegistry?.failedRetirements instanceof Map) ||
|
|
23
|
+
!(retirementRegistry?.disposedAgents instanceof WeakSet)) {
|
|
24
|
+
throw new Error('Invalid hello-cc Cordis retirement registry');
|
|
25
|
+
}
|
|
26
|
+
const { failedRetirements, disposedAgents } = retirementRegistry;
|
|
15
27
|
|
|
16
28
|
function retire(agent, state, { agentDisposed = false, contextDisposed = false }, forget) {
|
|
17
29
|
let entry = failedRetirements.get(agent);
|
|
@@ -22,17 +34,9 @@ function retire(agent, state, { agentDisposed = false, contextDisposed = false }
|
|
|
22
34
|
if (agentDisposed) entry.agentDisposed = true;
|
|
23
35
|
if (contextDisposed) entry.contextDisposed = true;
|
|
24
36
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
failedRetirements.delete(agent);
|
|
29
|
-
} catch (error) {
|
|
30
|
-
if (error?.code === 'DSH_COLLABORATION_CONFLICT') {
|
|
31
|
-
entry.forget();
|
|
32
|
-
failedRetirements.delete(agent);
|
|
33
|
-
}
|
|
34
|
-
throw error;
|
|
35
|
-
}
|
|
37
|
+
entry.state.dispose();
|
|
38
|
+
entry.forget();
|
|
39
|
+
failedRetirements.delete(agent);
|
|
36
40
|
}
|
|
37
41
|
|
|
38
42
|
function retryRetirements(candidate) {
|
package/lib/integrations/dsh.mjs
CHANGED
|
@@ -2,12 +2,14 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import process from 'node:process';
|
|
4
4
|
import { createHash, randomUUID } from 'node:crypto';
|
|
5
|
-
import {
|
|
5
|
+
import { spawnPinnedCwdProcess } from '../process/pinned-cwd.mjs';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
import { CliError } from '../shared/errors.mjs';
|
|
8
8
|
import { withFileLock } from '../shared/file-lock.mjs';
|
|
9
9
|
import { commandPath } from '../cli-runtime.mjs';
|
|
10
10
|
import { shellQuoteArg } from '../format.mjs';
|
|
11
|
+
import { resolveProjectDatabase, ensurePrivateProjectStateSubdirectory } from '../runtime/project-path.mjs';
|
|
12
|
+
import { unsafeDirectoryAcl } from '../runtime/project-trust.mjs';
|
|
11
13
|
|
|
12
14
|
export const DSH_BASELINE_VERSION = '0.2.0-rc.2';
|
|
13
15
|
export const DSH_INSTALL_HINT = `npm install -g @deepseek-ai/dsh@${DSH_BASELINE_VERSION}`;
|
|
@@ -42,16 +44,14 @@ function integrationPaths(ctx, create = false) {
|
|
|
42
44
|
try { root = fs.realpathSync(ctx.root); }
|
|
43
45
|
catch { throw new CliError('BAD_ARGS', `Project root does not exist: ${ctx.root}`); }
|
|
44
46
|
if (!fs.statSync(root).isDirectory()) throw new CliError('BAD_ARGS', `Project root is not a directory: ${root}`);
|
|
45
|
-
const stateDir =
|
|
47
|
+
const stateDir = resolveProjectDatabase({ root, createStateDir: create }).stateDir;
|
|
46
48
|
const directory = path.join(stateDir, 'dsh');
|
|
49
|
+
// DSH artifacts are opened and renamed by pathname. Their parent must be
|
|
50
|
+
// owned and non-rebindable by another OS user before either operation.
|
|
51
|
+
ensurePrivateProjectStateSubdirectory(root, 'dsh', { create });
|
|
47
52
|
const parents = [];
|
|
48
53
|
for (const dir of [root, stateDir, directory]) {
|
|
49
|
-
|
|
50
|
-
if (!stat && create) {
|
|
51
|
-
try { fs.mkdirSync(dir, { mode: 0o700 }); }
|
|
52
|
-
catch (error) { if (error.code !== 'EEXIST') throw error; }
|
|
53
|
-
stat = lstat(dir);
|
|
54
|
-
}
|
|
54
|
+
const stat = lstat(dir);
|
|
55
55
|
if (stat && (!stat.isDirectory() || stat.isSymbolicLink())) {
|
|
56
56
|
throw forbidden(dir, 'expected an ordinary directory, found a symlink or other object');
|
|
57
57
|
}
|
|
@@ -75,8 +75,11 @@ function readArtifact(paths, name) {
|
|
|
75
75
|
const file = path.join(paths.directory, name);
|
|
76
76
|
const stat = lstat(file);
|
|
77
77
|
if (!stat) return null;
|
|
78
|
-
if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1
|
|
79
|
-
|
|
78
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1 ||
|
|
79
|
+
(typeof process.getuid === 'function' && stat.uid !== process.getuid()) ||
|
|
80
|
+
(process.platform !== 'win32' && (stat.mode & 0o777) !== 0o600) ||
|
|
81
|
+
unsafeDirectoryAcl(file, stat)) {
|
|
82
|
+
throw forbidden(file, 'expected an owned private file with no symlink or hard-link alias');
|
|
80
83
|
}
|
|
81
84
|
const fd = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0));
|
|
82
85
|
try {
|
|
@@ -268,7 +271,9 @@ export async function launchDshWeb(ctx, options = {}) {
|
|
|
268
271
|
const env = createDshEnvironment(options.env || process.env, { dshHome: options.dshHome, cwd: ctx.cwd });
|
|
269
272
|
const args = ['web', '--patch', setup.patchPath, ...(options.args || [])];
|
|
270
273
|
return new Promise((resolve, reject) => {
|
|
271
|
-
const child =
|
|
274
|
+
const child = spawnPinnedCwdProcess(binary, args, {
|
|
275
|
+
cwd: setup.root, env, stdio: options.stdio || 'inherit'
|
|
276
|
+
});
|
|
272
277
|
const handlers = new Map();
|
|
273
278
|
const cleanup = () => {
|
|
274
279
|
for (const signal of handlers.keys()) process.removeListener(signal, handlers.get(signal));
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// Bounded MCP 2025-11-25 form schemas. This factory is also embedded in the Web
|
|
2
|
+
// responder so the UI and owning executor apply the same constraints.
|
|
3
|
+
export function createMcpFormValidator() {
|
|
4
|
+
const object = value => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
5
|
+
const own = (value, key) => Object.hasOwn(value, key);
|
|
6
|
+
function fail(message) { throw Object.assign(new Error(message), { code: 'INTERACTION_RESPONSE_INVALID' }); }
|
|
7
|
+
function shape(value, allowed) {
|
|
8
|
+
if (!object(value) || Object.keys(value).some(key => !allowed.includes(key))) fail('Unsupported MCP form schema');
|
|
9
|
+
}
|
|
10
|
+
function text(value) { return typeof value === 'string' && value.length <= 8192; }
|
|
11
|
+
function bound(value) { return Number.isSafeInteger(value) && value >= 0; }
|
|
12
|
+
function options(schema, titledKey, legacy = false) {
|
|
13
|
+
let values;
|
|
14
|
+
if (own(schema, 'enum')) {
|
|
15
|
+
if (!Array.isArray(schema.enum) || schema.enum.some(value => !text(value))) fail('Invalid MCP form options');
|
|
16
|
+
if (legacy && own(schema, 'enumNames') && (!Array.isArray(schema.enumNames) || schema.enumNames.length !== schema.enum.length || schema.enumNames.some(value => !text(value)))) fail('Invalid MCP option labels');
|
|
17
|
+
values = schema.enum.map((value, i) => ({ value, label: schema.enumNames?.[i] ?? value }));
|
|
18
|
+
} else {
|
|
19
|
+
if (!Array.isArray(schema[titledKey])) fail('Invalid MCP form options');
|
|
20
|
+
values = schema[titledKey].map(entry => {
|
|
21
|
+
shape(entry, ['const', 'title']);
|
|
22
|
+
if (!text(entry.const) || !text(entry.title)) fail('Invalid MCP form options');
|
|
23
|
+
return { value: entry.const, label: entry.title };
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
if (!values.length || values.length > 100 || new Set(values.map(entry => entry.value)).size !== values.length) fail('Invalid or excessive MCP form options');
|
|
27
|
+
return values;
|
|
28
|
+
}
|
|
29
|
+
function date(value) {
|
|
30
|
+
return (/^\d{4}-\d{2}-\d{2}$/).test(value) && Number.isFinite(Date.parse(value + 'T00:00:00Z')) && new Date(value + 'T00:00:00Z').toISOString().slice(0, 10) === value;
|
|
31
|
+
}
|
|
32
|
+
function formatted(value, format) {
|
|
33
|
+
if (format === 'email') {
|
|
34
|
+
const at = value.lastIndexOf('@'), local = value.slice(0, at), domain = value.slice(at + 1);
|
|
35
|
+
// Common dot-atom/quoted mailbox syntax and DNS labels; no network lookup.
|
|
36
|
+
const mailbox = (/^(?:[A-Za-z0-9!#$%&'*+\/=?^_`{|}~-]+(?:\.[A-Za-z0-9!#$%&'*+\/=?^_`{|}~-]+)*|"(?:[^"\\\r\n]|\\[^\r\n])*")$/).test(local);
|
|
37
|
+
const host = (/^[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?)*$/).test(domain);
|
|
38
|
+
return at > 0 && mailbox && host && domain.split('.').every(label => label.length <= 63);
|
|
39
|
+
}
|
|
40
|
+
if (format === 'uri') {
|
|
41
|
+
if (/[^\x21-\x7e]|["<>`{}|\\^]|%(?![\da-fA-F]{2})/.test(value)) return false;
|
|
42
|
+
try { return Boolean(new URL(value).protocol); } catch { return false; }
|
|
43
|
+
}
|
|
44
|
+
if (format === 'date') return date(value);
|
|
45
|
+
if (format === 'date-time') {
|
|
46
|
+
const match = /^(\d{4}-\d{2}-\d{2})[tT](\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(?:[zZ]|[+-](\d{2}):(\d{2}))$/.exec(value);
|
|
47
|
+
return Boolean(match && date(match[1]) && Number(match[2]) < 24 && Number(match[3]) < 60 && Number(match[4]) <= 60 && (!match[5] || (Number(match[5]) < 24 && Number(match[6]) < 60)));
|
|
48
|
+
}
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
function validateValue(field, value) {
|
|
52
|
+
const error = () => fail('Invalid value for MCP field: ' + field.key);
|
|
53
|
+
if (field.kind === 'multi') {
|
|
54
|
+
if (!Array.isArray(value) || value.length > 100 || value.length < (field.minItems ?? 0) || value.length > (field.maxItems ?? 100) || new Set(value).size !== value.length || value.some(entry => !field.options.some(option => option.value === entry))) error();
|
|
55
|
+
} else if (field.kind === 'enum') {
|
|
56
|
+
if (!field.options.some(option => option.value === value)) error();
|
|
57
|
+
} else if (field.type === 'boolean') {
|
|
58
|
+
if (typeof value !== 'boolean') error();
|
|
59
|
+
} else if (field.type === 'number' || field.type === 'integer') {
|
|
60
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || (field.type === 'integer' && !Number.isSafeInteger(value)) || (field.minimum != null && value < field.minimum) || (field.maximum != null && value > field.maximum)) error();
|
|
61
|
+
} else {
|
|
62
|
+
if (!text(value) || Array.from(value).length < (field.minLength ?? 0) || Array.from(value).length > (field.maxLength ?? 8192) || !formatted(value, field.format)) error();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function describe(params) {
|
|
66
|
+
if (params?.mode !== 'form') fail('This MCP request mode cannot be answered in Web; decline or cancel');
|
|
67
|
+
const schema = params.requestedSchema;
|
|
68
|
+
shape(schema, ['$schema', 'type', 'properties', 'required', 'additionalProperties']);
|
|
69
|
+
if (schema.type !== 'object' || !object(schema.properties) || (own(schema, '$schema') && !text(schema.$schema)) || (own(schema, 'additionalProperties') && typeof schema.additionalProperties !== 'boolean') || JSON.stringify(schema).length > 65536) fail('Unsupported MCP form schema');
|
|
70
|
+
const entries = Object.entries(schema.properties), required = own(schema, 'required') ? schema.required : [];
|
|
71
|
+
if (entries.length > 50 || !Array.isArray(required) || new Set(required).size !== required.length || required.some(key => typeof key !== 'string' || !own(schema.properties, key))) fail('Invalid or excessive MCP form fields');
|
|
72
|
+
const fields = entries.map(([key, definition]) => {
|
|
73
|
+
if (!key || key.length > 512 || !object(definition)) fail('Invalid MCP field identity');
|
|
74
|
+
const type = definition.type;
|
|
75
|
+
let allowed, kind = type;
|
|
76
|
+
if (type === 'string') {
|
|
77
|
+
kind = own(definition, 'enum') || own(definition, 'oneOf') ? 'enum' : 'string';
|
|
78
|
+
allowed = kind === 'enum' ? (own(definition, 'enum') ? ['enum', 'enumNames'] : ['oneOf']) : ['minLength', 'maxLength', 'format'];
|
|
79
|
+
} else if (type === 'array') { kind = 'multi'; allowed = ['items', 'minItems', 'maxItems']; }
|
|
80
|
+
else if (type === 'boolean') allowed = [];
|
|
81
|
+
else if (type === 'number' || type === 'integer') allowed = ['minimum', 'maximum'];
|
|
82
|
+
else fail('Nested objects or unsupported MCP field types cannot be answered in Web');
|
|
83
|
+
shape(definition, ['type', 'title', 'description', 'default', ...allowed]);
|
|
84
|
+
for (const property of ['title', 'description']) if (own(definition, property) && !text(definition[property])) fail('Invalid MCP field label');
|
|
85
|
+
const field = { ...definition, key, kind, required: required.includes(key) };
|
|
86
|
+
for (const property of ['minLength', 'maxLength', 'minItems', 'maxItems']) if (own(definition, property) && !bound(definition[property])) fail('Invalid MCP field bounds');
|
|
87
|
+
for (const property of ['minimum', 'maximum']) if (own(definition, property) && (typeof definition[property] !== 'number' || !Number.isFinite(definition[property]))) fail('Invalid MCP numeric bounds');
|
|
88
|
+
if ((definition.minLength ?? 0) > Math.min(definition.maxLength ?? 8192, 8192) || (definition.minimum != null && definition.maximum != null && definition.minimum > definition.maximum) || (own(definition, 'format') && !['email', 'uri', 'date', 'date-time'].includes(definition.format))) fail('Unsupported or unsatisfiable MCP field constraints');
|
|
89
|
+
if (kind === 'enum') field.options = options(definition, 'oneOf', true);
|
|
90
|
+
if (kind === 'multi') {
|
|
91
|
+
const items = definition.items;
|
|
92
|
+
if (own(items || {}, 'enum')) { shape(items, ['type', 'enum']); if (items.type !== 'string') fail('Unsupported MCP array items'); }
|
|
93
|
+
else shape(items, [own(items || {}, 'anyOf') ? 'anyOf' : 'oneOf']);
|
|
94
|
+
field.options = options(items, own(items, 'anyOf') ? 'anyOf' : 'oneOf');
|
|
95
|
+
if ((definition.minItems ?? 0) > Math.min(definition.maxItems ?? 100, field.options.length)) fail('Unsatisfiable MCP selection constraints');
|
|
96
|
+
}
|
|
97
|
+
if (own(definition, 'default')) validateValue(field, definition.default);
|
|
98
|
+
return field;
|
|
99
|
+
});
|
|
100
|
+
return { fields };
|
|
101
|
+
}
|
|
102
|
+
function validate(params, content) {
|
|
103
|
+
const { fields } = describe(params);
|
|
104
|
+
if (!object(content) || Object.keys(content).some(key => !fields.some(field => field.key === key))) fail('MCP form content must contain only requested fields');
|
|
105
|
+
const result = {};
|
|
106
|
+
for (const field of fields) {
|
|
107
|
+
if (!own(content, field.key)) { if (field.required) fail('Required MCP field is missing: ' + field.key); continue; }
|
|
108
|
+
validateValue(field, content[field.key]);
|
|
109
|
+
// Field names are untrusted. Define own properties without invoking setters.
|
|
110
|
+
Object.defineProperty(result, field.key, { value: Array.isArray(content[field.key]) ? [...content[field.key]] : content[field.key], enumerable: true, configurable: true, writable: true });
|
|
111
|
+
}
|
|
112
|
+
if (JSON.stringify(result).length > 65536) fail('MCP form content exceeds the response limit');
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
return { describe, validate };
|
|
116
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// The same bounded URL contract runs in the responder and browser. Acceptance
|
|
2
|
+
// acknowledges an out-of-band flow; it never proves authentication succeeded.
|
|
3
|
+
export function createMcpUrlValidator() {
|
|
4
|
+
function fail() {
|
|
5
|
+
throw Object.assign(new Error('MCP authorization needs a valid HTTPS URL (or loopback HTTP) and elicitation identity'), { code: 'INTERACTION_RESPONSE_INVALID' });
|
|
6
|
+
}
|
|
7
|
+
function describe(params) {
|
|
8
|
+
if (!params || params.mode !== 'url' || typeof params.elicitationId !== 'string' ||
|
|
9
|
+
!params.elicitationId.trim() || params.elicitationId.length > 512 || /[\u0000-\u001f\u007f]/.test(params.elicitationId) ||
|
|
10
|
+
typeof params.url !== 'string' || !params.url || params.url.length > 8192 || /[\\\s\u0000-\u001f\u007f]/.test(params.url)) fail();
|
|
11
|
+
let url;
|
|
12
|
+
try { url = new URL(params.url); } catch { fail(); }
|
|
13
|
+
if (url.username || url.password || !url.hostname || !['http:', 'https:'].includes(url.protocol) ||
|
|
14
|
+
(url.protocol === 'http:' && !['localhost', '127.0.0.1', '[::1]'].includes(url.hostname))) fail();
|
|
15
|
+
return { href: url.href, origin: url.origin, loopback: ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname) };
|
|
16
|
+
}
|
|
17
|
+
return { describe };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Pending request parameters exist in live memory only. Event histories must
|
|
21
|
+
// not retain authorization URLs, device codes, opaque ids, messages or metadata.
|
|
22
|
+
export function redactMcpUrlInteraction(request) {
|
|
23
|
+
if (request?.method !== 'mcpServer/elicitation/request' || request.params?.mode !== 'url') return request;
|
|
24
|
+
return { ...request, params: { mode: 'url', serverName: request.params.serverName } };
|
|
25
|
+
}
|
|
@@ -4,6 +4,8 @@ import path from 'node:path';
|
|
|
4
4
|
import { pathToFileURL } from 'node:url';
|
|
5
5
|
import { createNativeInteractions } from './interactions.mjs';
|
|
6
6
|
import { CliError } from '../../shared/errors.mjs';
|
|
7
|
+
import { claudeUsage } from './telemetry.mjs';
|
|
8
|
+
import { captureSelectedCwdIdentity } from '../../process/selected-cwd-identity.mjs';
|
|
7
9
|
|
|
8
10
|
const SESSION_MARKERS = new Set([
|
|
9
11
|
'CLAUDECODE', 'CLAUDE_CODE_SESSION_ID', 'CLAUDE_CODE_PARENT_SESSION_ID',
|
|
@@ -84,6 +86,7 @@ function withTimeout(promise, timeoutMs, operation) {
|
|
|
84
86
|
|
|
85
87
|
// This adapter owns an SDK subprocess. It does not attach to a running TUI.
|
|
86
88
|
export function createClaudeAdapter(options = {}) {
|
|
89
|
+
const cwdBinding = options.cwd ? captureSelectedCwdIdentity(options.cwd, options.expectedCwdIdentity) : null;
|
|
87
90
|
const capabilities = Object.freeze({
|
|
88
91
|
create: true, resume: true, send: true, observe: true,
|
|
89
92
|
interrupt: true, close: true, steer: false, fork: false, approvals: Boolean(options.interactive), mcp: Boolean(options.mcpServers)
|
|
@@ -94,6 +97,7 @@ export function createClaudeAdapter(options = {}) {
|
|
|
94
97
|
let sessionId = null;
|
|
95
98
|
let requestedSessionId = null;
|
|
96
99
|
let model;
|
|
100
|
+
let metrics = null, runtimeMetadata = {};
|
|
97
101
|
let queryFunction = options.query;
|
|
98
102
|
let sdkQuery = null;
|
|
99
103
|
let pump = null;
|
|
@@ -113,6 +117,7 @@ export function createClaudeAdapter(options = {}) {
|
|
|
113
117
|
provider: 'claude', status, sessionId,
|
|
114
118
|
turnId: activeTurn?.id || null,
|
|
115
119
|
submissionId: activeTurn?.submissionId || null,
|
|
120
|
+
metrics, runtimeMetadata: { ...runtimeMetadata },
|
|
116
121
|
capabilities, executorId: interactions.executorId, pendingApprovals: interactions.snapshot()
|
|
117
122
|
};
|
|
118
123
|
}
|
|
@@ -180,6 +185,9 @@ export function createClaudeAdapter(options = {}) {
|
|
|
180
185
|
return;
|
|
181
186
|
}
|
|
182
187
|
sessionId = initializedId;
|
|
188
|
+
runtimeMetadata = { ...(typeof message.model === 'string' ? { model: message.model } : {}),
|
|
189
|
+
...(typeof message.permissionMode === 'string' ? { permissionMode: message.permissionMode } : {}) };
|
|
190
|
+
emit('metadata', { runtimeMetadata: { ...runtimeMetadata } });
|
|
183
191
|
emit('status', { status, initialized: Boolean(sessionId) });
|
|
184
192
|
return;
|
|
185
193
|
}
|
|
@@ -207,6 +215,10 @@ export function createClaudeAdapter(options = {}) {
|
|
|
207
215
|
return;
|
|
208
216
|
}
|
|
209
217
|
if (message.type !== 'result') return;
|
|
218
|
+
if (belongs) {
|
|
219
|
+
const reported = claudeUsage(message, turn.id);
|
|
220
|
+
if (reported) { metrics = reported; emit('usage', { metrics }, turn); }
|
|
221
|
+
}
|
|
210
222
|
if (belongs) interactions.expire(turn.id);
|
|
211
223
|
if (!belongs) {
|
|
212
224
|
// Startup errors may precede input consumption and have no UUID echo.
|
|
@@ -242,6 +254,7 @@ export function createClaudeAdapter(options = {}) {
|
|
|
242
254
|
const resumeId = request.sessionId;
|
|
243
255
|
const selectedModel = request.model;
|
|
244
256
|
if (closed) throw new CliError('NATIVE_CLOSED', 'Claude native session is closed');
|
|
257
|
+
cwdBinding?.assertUnchanged();
|
|
245
258
|
if (opening) return opening;
|
|
246
259
|
if (status !== 'created') {
|
|
247
260
|
if ((resumeId && resumeId !== requestedSessionId) ||
|
|
@@ -307,6 +320,7 @@ export function createClaudeAdapter(options = {}) {
|
|
|
307
320
|
// Assign before invoking the factory so close() can observe and await any
|
|
308
321
|
// in-progress resource creation, including factories that resolve late.
|
|
309
322
|
startupPromise = Promise.resolve().then(async () => {
|
|
323
|
+
cwdBinding?.assertUnchanged();
|
|
310
324
|
sdkQuery = await queryFunction({
|
|
311
325
|
prompt: inputs,
|
|
312
326
|
options: {
|
|
@@ -372,6 +386,7 @@ export function createClaudeAdapter(options = {}) {
|
|
|
372
386
|
'Claude native session is busy; steering is unsupported. Wait for completion or interrupt it.');
|
|
373
387
|
if (expectedTurnId) throw new CliError('NATIVE_STALE_TURN', 'Claude native session has no active turn to steer');
|
|
374
388
|
if (queryEnded) throw new CliError('NATIVE_SDK_ENDED', 'Claude SDK stream has ended; open a new adapter to resume');
|
|
389
|
+
cwdBinding?.assertUnchanged();
|
|
375
390
|
const turn = {
|
|
376
391
|
id: randomUUID(), inputId: randomUUID(),
|
|
377
392
|
submissionId: submissionId || randomUUID(), consumed: false,
|
|
@@ -384,6 +399,7 @@ export function createClaudeAdapter(options = {}) {
|
|
|
384
399
|
if (closed || queryEnded || activeTurn !== turn) {
|
|
385
400
|
throw new CliError('NATIVE_SDK_ENDED', 'Claude SDK ended before accepting the submission');
|
|
386
401
|
}
|
|
402
|
+
cwdBinding?.assertUnchanged();
|
|
387
403
|
inputs.push({
|
|
388
404
|
type: 'user', uuid: turn.inputId,
|
|
389
405
|
...(sessionId ? { session_id: sessionId } : {}),
|
|
@@ -431,12 +447,14 @@ export function createClaudeAdapter(options = {}) {
|
|
|
431
447
|
// A deadline bounds callers, while the cleanup keeps waiting for a late
|
|
432
448
|
// factory and tears down its resource even if the caller already timed out.
|
|
433
449
|
const cleanup = (async () => {
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
450
|
+
try {
|
|
451
|
+
if (opening) await opening.catch(() => {});
|
|
452
|
+
if (startupPromise) await startupPromise.catch(() => {});
|
|
453
|
+
await teardownQuery();
|
|
454
|
+
if (pump) await pump;
|
|
455
|
+
setStatus('closed', turn ? { cancelled: true } : {}, turn);
|
|
456
|
+
return snapshot();
|
|
457
|
+
} finally { cwdBinding?.release(); }
|
|
440
458
|
})();
|
|
441
459
|
closePromise = withTimeout(cleanup, timeoutMs, 'close').catch((error) => {
|
|
442
460
|
// A caller may retry shutdown after the deadline or a teardown failure.
|