@gajae-code/coding-agent 0.11.9 → 0.11.10
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 +18 -1
- package/dist/types/config/model-registry.d.ts +2 -0
- package/dist/types/gjc-runtime/state-writer.d.ts +1 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +16 -8
- package/dist/types/sdk/bus/daemon-paths.d.ts +1 -0
- package/dist/types/sdk/bus/index.d.ts +8 -1
- package/dist/types/sdk/bus/operator-runtime.d.ts +1 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +12 -7
- package/dist/types/sdk/bus/telegram-daemon.d.ts +74 -5
- package/dist/types/sdk/session.d.ts +7 -0
- package/dist/types/session/agent-session.d.ts +2 -0
- package/dist/types/session/internal/managed-session-scope.d.ts +1 -1
- package/dist/types/task/provider-retry-status.d.ts +13 -0
- package/dist/types/task/types.d.ts +3 -0
- package/dist/types/tools/index.d.ts +3 -1
- package/package.json +8 -8
- package/scripts/generate-sdk-operation-inventory.ts +1 -0
- package/src/cli/notify-cli.ts +1 -0
- package/src/config/model-profiles.ts +7 -7
- package/src/config/model-registry.ts +4 -2
- package/src/eval/js/tool-bridge.ts +1 -1
- package/src/gjc-runtime/deep-interview-draft.ts +2 -1
- package/src/gjc-runtime/deep-interview-recorder.ts +16 -1
- package/src/gjc-runtime/state-writer.ts +1 -1
- package/src/hooks/skill-state.ts +23 -1
- package/src/internal-urls/docs-index.generated.ts +3 -3
- package/src/modes/controllers/selector-controller.ts +38 -8
- package/src/prompts/agent-fragments/restricted-bash.md +1 -1
- package/src/prompts/agents/architect.md +8 -1
- package/src/prompts/agents/critic.md +8 -1
- package/src/prompts/agents/planner.md +8 -1
- package/src/sdk/broker/identity.ts +22 -12
- package/src/sdk/bus/daemon-paths.ts +2 -0
- package/src/sdk/bus/index.ts +60 -5
- package/src/sdk/bus/notification-service.ts +19 -5
- package/src/sdk/bus/operator-runtime.ts +31 -5
- package/src/sdk/bus/telegram-cli.ts +6 -13
- package/src/sdk/bus/telegram-daemon-cli.ts +8 -3
- package/src/sdk/bus/telegram-daemon-contract.ts +13 -7
- package/src/sdk/bus/telegram-daemon-control.ts +14 -5
- package/src/sdk/bus/telegram-daemon.ts +667 -118
- package/src/sdk/protocol/operation-inventory.generated.json +11 -0
- package/src/sdk/session.ts +12 -0
- package/src/session/agent-session.ts +54 -10
- package/src/session/internal/managed-session-scope.ts +133 -117
- package/src/skill-state/workflow-mutation-guard.ts +41 -7
- package/src/task/executor.ts +141 -39
- package/src/task/provider-retry-status.ts +88 -0
- package/src/task/render.ts +45 -26
- package/src/task/types.ts +3 -0
- package/src/tools/bash-allowed-prefixes.ts +45 -1
- package/src/tools/index.ts +3 -1
- package/src/tools/subagent-render.ts +74 -16
- package/src/tools/subagent.ts +2 -0
- package/vendor/insane-search/MANIFEST.json +4 -1
- package/vendor/insane-search/engine/bias_check.py +5 -0
- package/vendor/insane-search/engine/fetch_chain.py +67 -1
- package/vendor/insane-search/engine/transport.py +13 -2
- package/vendor/insane-search/engine/validators.py +4 -1
|
@@ -2659,6 +2659,17 @@
|
|
|
2659
2659
|
"testIds": "not_applicable"
|
|
2660
2660
|
}
|
|
2661
2661
|
},
|
|
2662
|
+
{
|
|
2663
|
+
"sourceId": "agent_session:getToolForExecution",
|
|
2664
|
+
"sourceFile": "packages/coding-agent/src/session/agent-session.ts",
|
|
2665
|
+
"sourceKind": "agent_session",
|
|
2666
|
+
"decision": "exclude",
|
|
2667
|
+
"rationale": "internal accessor/plumbing, not a user-facing control seam",
|
|
2668
|
+
"exclusionMetadata": {
|
|
2669
|
+
"adapterMappings": "not_applicable",
|
|
2670
|
+
"testIds": "not_applicable"
|
|
2671
|
+
}
|
|
2672
|
+
},
|
|
2662
2673
|
{
|
|
2663
2674
|
"sourceId": "agent_session:registerForegroundBashBackgroundRequestHandler",
|
|
2664
2675
|
"sourceFile": "packages/coding-agent/src/session/agent-session.ts",
|
package/src/sdk/session.ts
CHANGED
|
@@ -428,6 +428,13 @@ export interface CreateAgentSessionOptions {
|
|
|
428
428
|
goalToolAllowedOps?: readonly ("create" | "get" | "complete" | "resume" | "drop" | "pause")[];
|
|
429
429
|
/** Optional per-session allowlist for tools exposed through search_tool_bm25. */
|
|
430
430
|
discoverableToolAllowedNames?: readonly string[];
|
|
431
|
+
/**
|
|
432
|
+
* Discoverable built-in tools that must stay in the initial active set even when
|
|
433
|
+
* `tools.discoveryMode === "all"` would otherwise hide them behind `search_tool_bm25`.
|
|
434
|
+
* Used for coordination tools (e.g. `irc`) that a subagent must be able to use
|
|
435
|
+
* immediately without first spending a discovery round-trip to find them.
|
|
436
|
+
*/
|
|
437
|
+
alwaysActiveToolNames?: readonly string[];
|
|
431
438
|
/** Optional shared agent registry for IRC routing. Default: AgentRegistry.global(). */
|
|
432
439
|
agentRegistry?: AgentRegistry;
|
|
433
440
|
/** Parent task ID prefix for nested artifact naming (e.g., "6-Extensions") */
|
|
@@ -1542,6 +1549,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1542
1549
|
goalToolAllowedOps: options.goalToolAllowedOps,
|
|
1543
1550
|
discoverableToolAllowedNames: options.discoverableToolAllowedNames,
|
|
1544
1551
|
getToolByName: name => session?.getToolByName(name),
|
|
1552
|
+
getToolForExecution: name => session?.getToolForExecution(name),
|
|
1545
1553
|
agentRegistry,
|
|
1546
1554
|
getSessionSpawns: () => options.spawns ?? "*",
|
|
1547
1555
|
getModelString: () => (hasExplicitModel && model ? formatModelString(model) : undefined),
|
|
@@ -2329,6 +2337,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2329
2337
|
// from the initial set unless they were explicitly requested or restored from persistence.
|
|
2330
2338
|
// The model finds them via search_tool_bm25 and activates them on demand.
|
|
2331
2339
|
if (effectiveDiscoveryMode === "all") {
|
|
2340
|
+
const alwaysActiveDiscoveredBuiltinNames = new Set(
|
|
2341
|
+
(options.alwaysActiveToolNames ?? []).map(name => name.toLowerCase()),
|
|
2342
|
+
);
|
|
2332
2343
|
const essentialBuiltinNames = new Set(computeEssentialBuiltinNames(settings));
|
|
2333
2344
|
const allowedDiscoveredBuiltinNames = options.discoverableToolAllowedNames
|
|
2334
2345
|
? new Set(options.discoverableToolAllowedNames.map(name => name.toLowerCase()))
|
|
@@ -2337,6 +2348,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2337
2348
|
const tool = toolRegistry.get(name);
|
|
2338
2349
|
if (!tool?.loadMode) return true; // not a built-in — leave MCP/custom/extension to existing logic
|
|
2339
2350
|
if (tool.loadMode === "essential") return true;
|
|
2351
|
+
if (alwaysActiveDiscoveredBuiltinNames.has(name)) return true;
|
|
2340
2352
|
return essentialBuiltinNames.has(name);
|
|
2341
2353
|
});
|
|
2342
2354
|
explicitlyRequestedDiscoveredBuiltinToolNames = selectRestorableDiscoveredBuiltinToolNames(
|
|
@@ -1048,12 +1048,16 @@ function createHandoffContext(document: string): string {
|
|
|
1048
1048
|
// ============================================================================
|
|
1049
1049
|
|
|
1050
1050
|
/** Tools that require user permission before execution when an ACP client is connected. */
|
|
1051
|
-
const PERMISSION_REQUIRED_TOOLS = new Set(["bash", "monitor", "edit", "delete", "move"]);
|
|
1051
|
+
const PERMISSION_REQUIRED_TOOLS = new Set(["bash", "monitor", "eval", "edit", "delete", "move"]);
|
|
1052
1052
|
|
|
1053
1053
|
function isShellExecutionPermissionTool(toolName: string): boolean {
|
|
1054
1054
|
return toolName === "bash" || toolName === "monitor";
|
|
1055
1055
|
}
|
|
1056
1056
|
|
|
1057
|
+
function isExecutionPermissionTool(toolName: string): boolean {
|
|
1058
|
+
return isShellExecutionPermissionTool(toolName) || toolName === "eval";
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1057
1061
|
/** Permission options presented to the client on each gated tool call. */
|
|
1058
1062
|
const PERMISSION_OPTIONS: ClientBridgePermissionOption[] = [
|
|
1059
1063
|
{ optionId: "allow_once", name: "Allow once", kind: "allow_once" },
|
|
@@ -1121,6 +1125,18 @@ function getPermissionIntent(
|
|
|
1121
1125
|
const cmd = getStringProperty(a, "command")?.slice(0, 80);
|
|
1122
1126
|
return { toolName, title: cmd || toolName, cacheKey: toolName };
|
|
1123
1127
|
}
|
|
1128
|
+
if (toolName === "eval") {
|
|
1129
|
+
const cells = Array.isArray(a.cells) ? a.cells : [];
|
|
1130
|
+
const firstCell = cells.find(cell => cell && typeof cell === "object" && !Array.isArray(cell));
|
|
1131
|
+
const cell = firstCell as Record<string, unknown> | undefined;
|
|
1132
|
+
const title = cell ? getStringProperty(cell, "title") : undefined;
|
|
1133
|
+
const language = cell ? getStringProperty(cell, "language") : undefined;
|
|
1134
|
+
return {
|
|
1135
|
+
toolName,
|
|
1136
|
+
title: title ?? (language ? `Eval ${language}` : "eval"),
|
|
1137
|
+
cacheKey: toolName,
|
|
1138
|
+
};
|
|
1139
|
+
}
|
|
1124
1140
|
if (toolName === "delete") {
|
|
1125
1141
|
const p = getStringProperty(a, "path");
|
|
1126
1142
|
return { toolName, title: p ? `Delete ${p}` : toolName, paths: p ? [p] : undefined, cacheKey: toolName };
|
|
@@ -5502,6 +5518,12 @@ export class AgentSession {
|
|
|
5502
5518
|
return undefined;
|
|
5503
5519
|
}
|
|
5504
5520
|
|
|
5521
|
+
/** Get a registered tool with the same guards used for model-facing execution. */
|
|
5522
|
+
getToolForExecution(name: string): AgentTool | undefined {
|
|
5523
|
+
const tool = this.getToolByName(name);
|
|
5524
|
+
return tool ? this.#prepareToolForExecution(tool) : undefined;
|
|
5525
|
+
}
|
|
5526
|
+
|
|
5505
5527
|
/**
|
|
5506
5528
|
* Register a UI/control-plane request handler for a currently foregrounded
|
|
5507
5529
|
* managed bash execution. This is intentionally narrower than generic
|
|
@@ -5766,6 +5788,7 @@ export class AgentSession {
|
|
|
5766
5788
|
return await target.execute(toolCallId, args as never, signal, onUpdate, ctx);
|
|
5767
5789
|
}
|
|
5768
5790
|
const isShellExecutionTool = isShellExecutionPermissionTool(target.name);
|
|
5791
|
+
const isExecutionTool = isExecutionPermissionTool(target.name);
|
|
5769
5792
|
const command =
|
|
5770
5793
|
isShellExecutionTool && args && typeof args === "object" && !Array.isArray(args)
|
|
5771
5794
|
? getStringProperty(args as Record<string, unknown>, "command")
|
|
@@ -5808,7 +5831,7 @@ export class AgentSession {
|
|
|
5808
5831
|
toolCallId,
|
|
5809
5832
|
toolName: target.name,
|
|
5810
5833
|
title: permissionIntent.title,
|
|
5811
|
-
...(
|
|
5834
|
+
...(isExecutionTool ? { kind: "execute" } : {}),
|
|
5812
5835
|
status: "pending",
|
|
5813
5836
|
rawInput: args,
|
|
5814
5837
|
...(commandContent ? { content: commandContent } : {}),
|
|
@@ -7530,7 +7553,11 @@ export class AgentSession {
|
|
|
7530
7553
|
}
|
|
7531
7554
|
|
|
7532
7555
|
// Validate API key
|
|
7533
|
-
const apiKey = await this.#
|
|
7556
|
+
const apiKey = await this.#awaitPromptPreflight(
|
|
7557
|
+
generation,
|
|
7558
|
+
preflightSignal,
|
|
7559
|
+
this.#modelRegistry.getApiKey(this.model, this.sessionId, { signal: preflightSignal }),
|
|
7560
|
+
);
|
|
7534
7561
|
if (!apiKey) {
|
|
7535
7562
|
throw new Error(formatNoCredentialOnboardingError(this.model.provider));
|
|
7536
7563
|
}
|
|
@@ -9013,6 +9040,10 @@ export class AgentSession {
|
|
|
9013
9040
|
this.agent.reset();
|
|
9014
9041
|
if (!options?.drop) await this.sessionManager.flush();
|
|
9015
9042
|
await this.sessionManager.newSession(options);
|
|
9043
|
+
// Gate the successor local:// root before the successor identity is
|
|
9044
|
+
// published to the agent, the workflow-gate emitter, hooks, or any
|
|
9045
|
+
// synchronous resolver (#2797 / #2925).
|
|
9046
|
+
await initializeLocalRoot(this.#localProtocolOptions());
|
|
9016
9047
|
this.setTodoPhases([]);
|
|
9017
9048
|
this.#syncAgentSessionId();
|
|
9018
9049
|
this.#bindWorkflowGateEmitter(previousWorkflowGateSessionId);
|
|
@@ -9080,6 +9111,10 @@ export class AgentSession {
|
|
|
9080
9111
|
this.#rebindProviderSessionState(new Map());
|
|
9081
9112
|
this.agent.reset();
|
|
9082
9113
|
await this.sessionManager.newSession(options);
|
|
9114
|
+
// Gate the successor local:// root before the successor identity is
|
|
9115
|
+
// published to the agent, the workflow-gate emitter, hooks, or any
|
|
9116
|
+
// synchronous resolver (#2797 / #2925).
|
|
9117
|
+
await initializeLocalRoot(this.#localProtocolOptions());
|
|
9083
9118
|
this.setTodoPhases([]);
|
|
9084
9119
|
this.#syncAgentSessionId();
|
|
9085
9120
|
this.#bindWorkflowGateEmitter(previousWorkflowGateSessionId);
|
|
@@ -9223,6 +9258,9 @@ export class AgentSession {
|
|
|
9223
9258
|
return false;
|
|
9224
9259
|
}
|
|
9225
9260
|
|
|
9261
|
+
// Fork rotates the session identity (and copies artifacts); gate the
|
|
9262
|
+
// successor local:// root before that identity is published (#2797 / #2925).
|
|
9263
|
+
await initializeLocalRoot(this.#localProtocolOptions());
|
|
9226
9264
|
// Update agent session ID
|
|
9227
9265
|
this.#syncAgentSessionId();
|
|
9228
9266
|
this.#bindWorkflowGateEmitter(previousWorkflowGateSessionId);
|
|
@@ -10630,6 +10668,10 @@ export class AgentSession {
|
|
|
10630
10668
|
previousSessionFile ? { parentSession: previousSessionFile } : undefined,
|
|
10631
10669
|
);
|
|
10632
10670
|
successorSessionFile = this.sessionFile;
|
|
10671
|
+
// Gate the successor local:// root inside the reversible prepare
|
|
10672
|
+
// window, before the successor identity is published; a gate failure
|
|
10673
|
+
// here rolls back (#2797 / #2925).
|
|
10674
|
+
await initializeLocalRoot(this.#localProtocolOptions());
|
|
10633
10675
|
this.agent.reset();
|
|
10634
10676
|
this.#syncAgentSessionId();
|
|
10635
10677
|
this.#rekeyHindsightMemoryForCurrentSessionId();
|
|
@@ -14557,6 +14599,10 @@ export class AgentSession {
|
|
|
14557
14599
|
|
|
14558
14600
|
try {
|
|
14559
14601
|
await this.sessionManager.setSessionFile(sessionPath);
|
|
14602
|
+
// The successor identity is already rotated in the manager but not yet
|
|
14603
|
+
// published; gate its local:// root before publication so no observer
|
|
14604
|
+
// can resolve against an ungated root (#2797 / #2925).
|
|
14605
|
+
if (switchingToDifferentSession) await initializeLocalRoot(this.#localProtocolOptions());
|
|
14560
14606
|
this.#syncAgentSessionId();
|
|
14561
14607
|
this.#rekeyHindsightMemoryForCurrentSessionId();
|
|
14562
14608
|
|
|
@@ -14633,13 +14679,8 @@ export class AgentSession {
|
|
|
14633
14679
|
await this.sessionManager.ensureOnDisk();
|
|
14634
14680
|
|
|
14635
14681
|
if (switchingToDifferentSession) {
|
|
14636
|
-
//
|
|
14637
|
-
//
|
|
14638
|
-
// post-commit session_switch / local path resolution. createAgentSession
|
|
14639
|
-
// already does this at cold start (#2797); switchSession previously omitted
|
|
14640
|
-
// it, so resolving local:// after /resume could fail-closed with
|
|
14641
|
-
// "legacy migration must complete before path resolution" (#2925).
|
|
14642
|
-
await initializeLocalRoot(this.#localProtocolOptions());
|
|
14682
|
+
// The local:// migration gate for this successor already ran above,
|
|
14683
|
+
// before the identity was published (#2797 / #2925).
|
|
14643
14684
|
this.#resetHindsightConversationTrackingIfHindsight();
|
|
14644
14685
|
this.#resetIrcRosterDeliveryState();
|
|
14645
14686
|
}
|
|
@@ -14771,6 +14812,9 @@ export class AgentSession {
|
|
|
14771
14812
|
} else {
|
|
14772
14813
|
this.sessionManager.createBranchedSession(selectedEntry.parentId);
|
|
14773
14814
|
}
|
|
14815
|
+
// Branch/tree-jump establishes a new session identity; gate the successor
|
|
14816
|
+
// local:// root before that identity is published (#2797 / #2925).
|
|
14817
|
+
await initializeLocalRoot(this.#localProtocolOptions());
|
|
14774
14818
|
this.#syncTodoPhasesFromBranch();
|
|
14775
14819
|
this.#syncAgentSessionId();
|
|
14776
14820
|
this.#bindWorkflowGateEmitter(previousWorkflowGateSessionId);
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
verifyOwnerOnlyPathSecurity,
|
|
9
9
|
verifyOwnerOnlyPathSecurityExpected,
|
|
10
10
|
} from "@gajae-code/natives";
|
|
11
|
-
import { pathIsWithin } from "@gajae-code/utils";
|
|
11
|
+
import { logger, pathIsWithin } from "@gajae-code/utils";
|
|
12
12
|
import type { ResumeSessionIdentity } from "../session-manager";
|
|
13
13
|
import {
|
|
14
14
|
FileSessionStorage,
|
|
@@ -2789,7 +2789,10 @@ function validateCandidateForScope(scope: ManagedScope, candidate: ManagedCandid
|
|
|
2789
2789
|
}
|
|
2790
2790
|
|
|
2791
2791
|
/** Resume tombstoned cleanup under its original operation lease without restoring retired candidates. */
|
|
2792
|
-
export async function reconcileManagedTombstones(
|
|
2792
|
+
export async function reconcileManagedTombstones(
|
|
2793
|
+
scope: ManagedScope,
|
|
2794
|
+
expectedCandidate?: ManagedCandidate,
|
|
2795
|
+
): Promise<void> {
|
|
2793
2796
|
const directory = path.join(managedInternalDirectory(scope), MANAGED_TOMBSTONES_DIRECTORY);
|
|
2794
2797
|
for (const name of fs.readdirSync(directory)) {
|
|
2795
2798
|
const tombstone = path.join(directory, name);
|
|
@@ -2807,135 +2810,148 @@ export async function reconcileManagedTombstones(scope: ManagedScope): Promise<v
|
|
|
2807
2810
|
for (const target of lockedTargets) {
|
|
2808
2811
|
lock.assertOwned();
|
|
2809
2812
|
if (cleanupCompleted(scope, tombstone, target)) continue;
|
|
2810
|
-
const pending = pendingCleanupReceipt(scope, tombstone, target);
|
|
2811
|
-
const observedPending = pending ? probePlannedCleanupDetach(target, pending) : undefined;
|
|
2812
2813
|
try {
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
(
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
cwd: target.cwd,
|
|
2844
|
-
transcriptIdentity: target.identity,
|
|
2845
|
-
expectedArtifactsIdentity: active.expectedArtifactsIdentity,
|
|
2846
|
-
expectedArtifactsTree: active.expectedArtifactsTree,
|
|
2847
|
-
detachedArtifactsPath:
|
|
2848
|
-
active.detachedArtifactsPath ??
|
|
2849
|
-
observedPending?.detachedArtifactsPath ??
|
|
2850
|
-
(fs.existsSync(active.plannedArtifactsPath) ? active.plannedArtifactsPath : undefined),
|
|
2851
|
-
detachedTranscriptPath:
|
|
2852
|
-
active.detachedTranscriptPath ??
|
|
2853
|
-
observedPending?.detachedTranscriptPath ??
|
|
2854
|
-
(pending && fs.existsSync(pending.plannedTranscriptPath)
|
|
2855
|
-
? pending.plannedTranscriptPath
|
|
2856
|
-
: undefined) ??
|
|
2857
|
-
(fs.existsSync(active.plannedTranscriptPath) ? active.plannedTranscriptPath : undefined),
|
|
2858
|
-
retainedArtifactsSuccessorPath: active.retainedArtifactsSuccessorPath,
|
|
2859
|
-
retainedArtifactsPlaceholderPath: active.retainedArtifactsPlaceholderPath,
|
|
2860
|
-
retainedArtifactsUnknownPath: active.retainedArtifactsUnknownPath,
|
|
2861
|
-
retainedTranscriptSuccessorPath: active.retainedTranscriptSuccessorPath,
|
|
2862
|
-
retainedTranscriptPlaceholderPath: active.retainedTranscriptPlaceholderPath,
|
|
2863
|
-
retainedTranscriptUnknownPath: active.retainedTranscriptUnknownPath,
|
|
2864
|
-
plannedArtifactsPath: active.plannedArtifactsPath,
|
|
2865
|
-
plannedTranscriptPath: active.plannedTranscriptPath,
|
|
2866
|
-
...(cleanupArtifactsRemoved(scope, tombstone, target, pending?.attempt ?? active.attempt)
|
|
2867
|
-
? { artifactsRemoved: true as const }
|
|
2868
|
-
: {}),
|
|
2869
|
-
});
|
|
2870
|
-
if (deletion.kind === "artifacts_removed") {
|
|
2871
|
-
await publishCleanupArtifactsRemoved(scope, tombstone, active, lock);
|
|
2872
|
-
deletion = await new FileSessionStorage().deleteSessionVerified({
|
|
2814
|
+
const pending = pendingCleanupReceipt(scope, tombstone, target);
|
|
2815
|
+
const observedPending = pending ? probePlannedCleanupDetach(target, pending) : undefined;
|
|
2816
|
+
try {
|
|
2817
|
+
fs.lstatSync(target.path);
|
|
2818
|
+
} catch (error) {
|
|
2819
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
2820
|
+
if (!observedPending) continue;
|
|
2821
|
+
if (
|
|
2822
|
+
cleanupArtifactsRemoved(scope, tombstone, target, observedPending.attempt) &&
|
|
2823
|
+
cleanupRootsAbsent(tombstone, target, observedPending)
|
|
2824
|
+
) {
|
|
2825
|
+
fsyncManagedParent(target.path);
|
|
2826
|
+
await publishCleanupCompleted(scope, tombstone, target, lock);
|
|
2827
|
+
continue;
|
|
2828
|
+
}
|
|
2829
|
+
} else throw error;
|
|
2830
|
+
}
|
|
2831
|
+
const verified = observedPending ? target : validateCandidateForScope(scope, target);
|
|
2832
|
+
if (!verified || !sameCandidate(verified, target)) throw new Error("source_changed");
|
|
2833
|
+
const discoveredDetach =
|
|
2834
|
+
!!observedPending &&
|
|
2835
|
+
(observedPending.detachedArtifactsPath !== pending?.detachedArtifactsPath ||
|
|
2836
|
+
observedPending.detachedTranscriptPath !== pending?.detachedTranscriptPath);
|
|
2837
|
+
const active =
|
|
2838
|
+
discoveredDetach || (observedPending && requiresFreshCleanupPlan(observedPending))
|
|
2839
|
+
? nextCleanupReceipt(target, observedPending)
|
|
2840
|
+
: (observedPending ?? nextCleanupReceipt(target, undefined));
|
|
2841
|
+
if (!observedPending || discoveredDetach || requiresFreshCleanupPlan(observedPending))
|
|
2842
|
+
await publishCleanupPending(scope, tombstone, active, lock);
|
|
2843
|
+
let deletion = await new FileSessionStorage().deleteSessionVerified({
|
|
2873
2844
|
sessionsRoot: scope.sessionsRoot,
|
|
2874
2845
|
transcriptPath: target.path,
|
|
2875
2846
|
sessionId: target.sessionId,
|
|
2876
2847
|
cwd: target.cwd,
|
|
2877
2848
|
transcriptIdentity: target.identity,
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2849
|
+
expectedArtifactsIdentity: active.expectedArtifactsIdentity,
|
|
2850
|
+
expectedArtifactsTree: active.expectedArtifactsTree,
|
|
2851
|
+
detachedArtifactsPath:
|
|
2852
|
+
active.detachedArtifactsPath ??
|
|
2853
|
+
observedPending?.detachedArtifactsPath ??
|
|
2854
|
+
(fs.existsSync(active.plannedArtifactsPath) ? active.plannedArtifactsPath : undefined),
|
|
2855
|
+
detachedTranscriptPath:
|
|
2856
|
+
active.detachedTranscriptPath ??
|
|
2857
|
+
observedPending?.detachedTranscriptPath ??
|
|
2858
|
+
(pending && fs.existsSync(pending.plannedTranscriptPath)
|
|
2859
|
+
? pending.plannedTranscriptPath
|
|
2860
|
+
: undefined) ??
|
|
2861
|
+
(fs.existsSync(active.plannedTranscriptPath) ? active.plannedTranscriptPath : undefined),
|
|
2882
2862
|
retainedArtifactsSuccessorPath: active.retainedArtifactsSuccessorPath,
|
|
2883
2863
|
retainedArtifactsPlaceholderPath: active.retainedArtifactsPlaceholderPath,
|
|
2884
2864
|
retainedArtifactsUnknownPath: active.retainedArtifactsUnknownPath,
|
|
2885
2865
|
retainedTranscriptSuccessorPath: active.retainedTranscriptSuccessorPath,
|
|
2886
2866
|
retainedTranscriptPlaceholderPath: active.retainedTranscriptPlaceholderPath,
|
|
2887
2867
|
retainedTranscriptUnknownPath: active.retainedTranscriptUnknownPath,
|
|
2888
|
-
|
|
2868
|
+
plannedArtifactsPath: active.plannedArtifactsPath,
|
|
2869
|
+
plannedTranscriptPath: active.plannedTranscriptPath,
|
|
2870
|
+
...(cleanupArtifactsRemoved(scope, tombstone, target, pending?.attempt ?? active.attempt)
|
|
2871
|
+
? { artifactsRemoved: true as const }
|
|
2872
|
+
: {}),
|
|
2889
2873
|
});
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2874
|
+
if (deletion.kind === "artifacts_removed") {
|
|
2875
|
+
await publishCleanupArtifactsRemoved(scope, tombstone, active, lock);
|
|
2876
|
+
deletion = await new FileSessionStorage().deleteSessionVerified({
|
|
2877
|
+
sessionsRoot: scope.sessionsRoot,
|
|
2878
|
+
transcriptPath: target.path,
|
|
2879
|
+
sessionId: target.sessionId,
|
|
2880
|
+
cwd: target.cwd,
|
|
2881
|
+
transcriptIdentity: target.identity,
|
|
2882
|
+
plannedArtifactsPath: active.plannedArtifactsPath,
|
|
2883
|
+
plannedTranscriptPath: active.plannedTranscriptPath,
|
|
2884
|
+
detachedTranscriptPath: active.detachedTranscriptPath ?? observedPending?.detachedTranscriptPath,
|
|
2885
|
+
|
|
2886
|
+
retainedArtifactsSuccessorPath: active.retainedArtifactsSuccessorPath,
|
|
2887
|
+
retainedArtifactsPlaceholderPath: active.retainedArtifactsPlaceholderPath,
|
|
2888
|
+
retainedArtifactsUnknownPath: active.retainedArtifactsUnknownPath,
|
|
2889
|
+
retainedTranscriptSuccessorPath: active.retainedTranscriptSuccessorPath,
|
|
2890
|
+
retainedTranscriptPlaceholderPath: active.retainedTranscriptPlaceholderPath,
|
|
2891
|
+
retainedTranscriptUnknownPath: active.retainedTranscriptUnknownPath,
|
|
2892
|
+
artifactsRemoved: true,
|
|
2893
|
+
});
|
|
2894
|
+
}
|
|
2895
|
+
if (deletion.kind === "cleanup_pending") {
|
|
2896
|
+
const retry = nextCleanupReceipt(target, active);
|
|
2897
|
+
await publishCleanupPending(
|
|
2898
|
+
scope,
|
|
2899
|
+
tombstone,
|
|
2900
|
+
{
|
|
2901
|
+
...retry,
|
|
2902
|
+
expectedArtifactsIdentity:
|
|
2903
|
+
deletion.phase === "artifacts"
|
|
2904
|
+
? deletion.artifactsIdentity
|
|
2905
|
+
: active.expectedArtifactsIdentity,
|
|
2906
|
+
expectedArtifactsTree:
|
|
2907
|
+
deletion.phase === "artifacts" ? deletion.artifactsTree : active.expectedArtifactsTree,
|
|
2908
|
+
detachedArtifactsPath:
|
|
2909
|
+
deletion.phase === "artifacts"
|
|
2910
|
+
? deletion.detachedArtifactsPath
|
|
2911
|
+
: active.detachedArtifactsPath,
|
|
2912
|
+
detachedTranscriptPath:
|
|
2913
|
+
deletion.phase === "transcript"
|
|
2914
|
+
? deletion.detachedTranscriptPath
|
|
2915
|
+
: active.detachedTranscriptPath,
|
|
2916
|
+
retainedArtifactsSuccessorPath:
|
|
2917
|
+
deletion.phase === "artifacts"
|
|
2918
|
+
? deletion.retainedSuccessorPath
|
|
2919
|
+
: active.retainedArtifactsSuccessorPath,
|
|
2920
|
+
retainedArtifactsPlaceholderPath:
|
|
2921
|
+
deletion.phase === "artifacts"
|
|
2922
|
+
? deletion.retainedPlaceholderPath
|
|
2923
|
+
: active.retainedArtifactsPlaceholderPath,
|
|
2924
|
+
retainedArtifactsUnknownPath:
|
|
2925
|
+
deletion.phase === "artifacts"
|
|
2926
|
+
? deletion.retainedUnknownPath
|
|
2927
|
+
: active.retainedArtifactsUnknownPath,
|
|
2928
|
+
retainedTranscriptSuccessorPath:
|
|
2929
|
+
deletion.phase === "transcript"
|
|
2930
|
+
? deletion.retainedSuccessorPath
|
|
2931
|
+
: active.retainedTranscriptSuccessorPath,
|
|
2932
|
+
retainedTranscriptPlaceholderPath:
|
|
2933
|
+
deletion.phase === "transcript"
|
|
2934
|
+
? deletion.retainedPlaceholderPath
|
|
2935
|
+
: active.retainedTranscriptPlaceholderPath,
|
|
2936
|
+
retainedTranscriptUnknownPath:
|
|
2937
|
+
deletion.phase === "transcript"
|
|
2938
|
+
? deletion.retainedUnknownPath
|
|
2939
|
+
: active.retainedTranscriptUnknownPath,
|
|
2940
|
+
},
|
|
2941
|
+
lock,
|
|
2942
|
+
);
|
|
2943
|
+
continue;
|
|
2944
|
+
}
|
|
2945
|
+
fsyncManagedParent(target.path);
|
|
2946
|
+
await publishCleanupCompleted(scope, tombstone, target, lock);
|
|
2947
|
+
} catch (error) {
|
|
2948
|
+
if (!expectedCandidate || target.sessionId === expectedCandidate.sessionId) throw error;
|
|
2949
|
+
logger.warn("Tombstone reconciliation failed for one target; will retry on a future scope open", {
|
|
2895
2950
|
tombstone,
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
deletion.phase === "artifacts" ? deletion.artifactsIdentity : active.expectedArtifactsIdentity,
|
|
2900
|
-
expectedArtifactsTree:
|
|
2901
|
-
deletion.phase === "artifacts" ? deletion.artifactsTree : active.expectedArtifactsTree,
|
|
2902
|
-
detachedArtifactsPath:
|
|
2903
|
-
deletion.phase === "artifacts" ? deletion.detachedArtifactsPath : active.detachedArtifactsPath,
|
|
2904
|
-
detachedTranscriptPath:
|
|
2905
|
-
deletion.phase === "transcript"
|
|
2906
|
-
? deletion.detachedTranscriptPath
|
|
2907
|
-
: active.detachedTranscriptPath,
|
|
2908
|
-
retainedArtifactsSuccessorPath:
|
|
2909
|
-
deletion.phase === "artifacts"
|
|
2910
|
-
? deletion.retainedSuccessorPath
|
|
2911
|
-
: active.retainedArtifactsSuccessorPath,
|
|
2912
|
-
retainedArtifactsPlaceholderPath:
|
|
2913
|
-
deletion.phase === "artifacts"
|
|
2914
|
-
? deletion.retainedPlaceholderPath
|
|
2915
|
-
: active.retainedArtifactsPlaceholderPath,
|
|
2916
|
-
retainedArtifactsUnknownPath:
|
|
2917
|
-
deletion.phase === "artifacts"
|
|
2918
|
-
? deletion.retainedUnknownPath
|
|
2919
|
-
: active.retainedArtifactsUnknownPath,
|
|
2920
|
-
retainedTranscriptSuccessorPath:
|
|
2921
|
-
deletion.phase === "transcript"
|
|
2922
|
-
? deletion.retainedSuccessorPath
|
|
2923
|
-
: active.retainedTranscriptSuccessorPath,
|
|
2924
|
-
retainedTranscriptPlaceholderPath:
|
|
2925
|
-
deletion.phase === "transcript"
|
|
2926
|
-
? deletion.retainedPlaceholderPath
|
|
2927
|
-
: active.retainedTranscriptPlaceholderPath,
|
|
2928
|
-
retainedTranscriptUnknownPath:
|
|
2929
|
-
deletion.phase === "transcript"
|
|
2930
|
-
? deletion.retainedUnknownPath
|
|
2931
|
-
: active.retainedTranscriptUnknownPath,
|
|
2932
|
-
},
|
|
2933
|
-
lock,
|
|
2934
|
-
);
|
|
2935
|
-
continue;
|
|
2951
|
+
sessionId: target.sessionId,
|
|
2952
|
+
error: String(error),
|
|
2953
|
+
});
|
|
2936
2954
|
}
|
|
2937
|
-
fsyncManagedParent(target.path);
|
|
2938
|
-
await publishCleanupCompleted(scope, tombstone, target, lock);
|
|
2939
2955
|
}
|
|
2940
2956
|
} finally {
|
|
2941
2957
|
if (lock) await lock.release().catch(() => undefined);
|
|
@@ -2962,7 +2978,7 @@ export async function prepareManagedSessionScopeForWrite(
|
|
|
2962
2978
|
ensureManagedDirectory(path.join(internal, MANAGED_LOCKS_DIRECTORY), root, policy);
|
|
2963
2979
|
ensureManagedDirectory(path.join(internal, MANAGED_RECEIPTS_DIRECTORY), root, policy);
|
|
2964
2980
|
ensureManagedDirectory(path.join(internal, MANAGED_TOMBSTONES_DIRECTORY), root, policy);
|
|
2965
|
-
await reconcileManagedTombstones(scope);
|
|
2981
|
+
await reconcileManagedTombstones(scope, expectedCandidate);
|
|
2966
2982
|
return { kind: "resolved", scope };
|
|
2967
2983
|
} catch (error) {
|
|
2968
2984
|
const publication = error instanceof ManagedPublishError ? error : undefined;
|
|
@@ -35,17 +35,28 @@ function planningPhaseBlockMessage(skill: CanonicalGjcWorkflowSkill): string {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
const BLOCKED_TOOL_NAMES = new Set(["edit", "write", "ast_edit", "bash"]);
|
|
38
|
+
/**
|
|
39
|
+
* Only `/dev/null` is exempt. `/dev/stdout`, `/dev/stderr`, and `/dev/fd/<n>` are descriptor
|
|
40
|
+
* aliases: `exec 1<>src/product.ts; printf x >/dev/stdout` reaches a real repository file
|
|
41
|
+
* through a rebound descriptor, so they must stay blocked.
|
|
42
|
+
*/
|
|
43
|
+
const DEVICE_SINK_PATHS = new Set(["/dev/null"]);
|
|
44
|
+
/** Bash write forms the plain `>`/`>>` scanner misses: clobber, dup-to-path, and read-write open. */
|
|
45
|
+
const BASH_EXTENDED_WRITE_RE = /(?:>\||<>|>&(?![\s;&|]*(?:\d+|-)(?:[\s;&|]|$)))\s*([^\s;&|]+)/g;
|
|
46
|
+
/** Descriptor rebinding is not statically resolvable; any `exec` redirection forces a fail-closed verdict. */
|
|
47
|
+
const BASH_EXEC_REDIRECT_RE = /(?:^|[;&|\n])\s*(?:sudo\s+)?exec\b[^;&|\n]*[<>]/i;
|
|
48
|
+
|
|
38
49
|
const ARCHIVE_OR_SQLITE_BASE_RE = /^(.+?\.(?:tar\.gz|sqlite3|sqlite|db3|zip|tgz|tar|db))(?:$|:)/i;
|
|
39
50
|
const INTERNAL_SCHEME_RE = /^[a-z][a-z0-9+.-]*:\/\//i;
|
|
40
51
|
const VIM_FILE_SWITCH_RE = /^\s*:(?:e|e!|edit|edit!)(?:\s+([^<\r\n]+))?(?:<CR>|\r|\n|$)/i;
|
|
41
52
|
const BASH_MUTATION_COMMAND_RE =
|
|
42
|
-
/(?:^|[;&|\n])\s*(?:\w+=[^\s]+\s+)*(?:sudo\s+)?(?:tee|touch|rm|mkdir|cp|mv|install|truncate)\b([^;&|\n]*)|(?:^|[^<>])(?:>>?|\d>>?)\s*([^\s;&|]+)/gi;
|
|
53
|
+
/(?:^|[;&|\n])\s*(?:\w+=[^\s]+\s+)*(?:sudo\s+)?(?:[^\s;&|]*\/)?(?:tee|touch|rm|mkdir|cp|mv|install|truncate)\b([^;&|\n]*)|(?:^|[^<>])(?:>>?|\d>>?)\s*([^\s;&|]+)/gi;
|
|
43
54
|
const BASH_IN_PLACE_MUTATION_COMMAND_RE = /(?:^|[;&|\n])\s*(?:\w+=[^\s]+\s+)*(?:sudo\s+)?(?:sed|perl)\b([^;&|\n]*)/gi;
|
|
44
55
|
const BASH_OPAQUE_INTERPRETER_WRITE_RE =
|
|
45
56
|
/(?:^|[;&|\n])\s*(?:\w+=[^\s]+\s+)*(?:sudo\s+)?(?:python3?|node|ruby)\b[^;&|\n]*(?:-c|-e)\b[^;&|\n]*(?:open\s*\(|writeFile(?:Sync)?\s*\(|\.write\s*\()/i;
|
|
46
57
|
const BASH_HEREDOC_OPAQUE_INTERPRETER_WRITE_RE =
|
|
47
58
|
/(?:^|[;&|\n])\s*(?:\w+=[^\s]+\s+)*(?:sudo\s+)?(?:python3?|node|ruby)\b[^;&|\n]*(?:<<[-]?\s*['"]?\w+['"]?)[\s\S]*(?:open\s*\(|writeFile(?:Sync)?\s*\(|\.write\s*\()/i;
|
|
48
|
-
const BASH_DD_OUTPUT_RE = /(?:^|[;&|\n])\s*(?:\w+=[^\s]+\s+)*(?:sudo\s+)?dd\b([^;&|\n]*)/gi;
|
|
59
|
+
const BASH_DD_OUTPUT_RE = /(?:^|[;&|\n])\s*(?:\w+=[^\s]+\s+)*(?:sudo\s+)?(?:[^\s;&|]*\/)?dd\b([^;&|\n]*)/gi;
|
|
49
60
|
|
|
50
61
|
type ToolWithEditMode = AgentTool & {
|
|
51
62
|
mode?: unknown;
|
|
@@ -371,6 +382,10 @@ function cleanShellWord(value: string): string {
|
|
|
371
382
|
return value.replace(/^['"]|['"]$/g, "");
|
|
372
383
|
}
|
|
373
384
|
|
|
385
|
+
function isDeviceSinkPath(value: string): boolean {
|
|
386
|
+
return DEVICE_SINK_PATHS.has(cleanShellWord(value));
|
|
387
|
+
}
|
|
388
|
+
|
|
374
389
|
function extractBashTargets(args: unknown): ExtractedTargets {
|
|
375
390
|
const record = getRecord(args);
|
|
376
391
|
const command = safeString(record?.command);
|
|
@@ -382,11 +397,25 @@ function extractBashTargets(args: unknown): ExtractedTargets {
|
|
|
382
397
|
if (BASH_OPAQUE_INTERPRETER_WRITE_RE.test(command) || BASH_HEREDOC_OPAQUE_INTERPRETER_WRITE_RE.test(command)) {
|
|
383
398
|
targets.unknown = true;
|
|
384
399
|
}
|
|
400
|
+
// `exec 1<>file` rebinds a descriptor, so a later `>/dev/null` may not reach the device at all.
|
|
401
|
+
if (BASH_EXEC_REDIRECT_RE.test(command)) {
|
|
402
|
+
targets.unknown = true;
|
|
403
|
+
}
|
|
404
|
+
for (const match of command.matchAll(BASH_EXTENDED_WRITE_RE)) {
|
|
405
|
+
const cleaned = cleanShellWord(match[1] ?? "");
|
|
406
|
+
if (!cleaned) targets.unknown = true;
|
|
407
|
+
else if (!isDeviceSinkPath(cleaned)) addPath(targets, cleaned);
|
|
408
|
+
}
|
|
385
409
|
for (const match of command.matchAll(BASH_DD_OUTPUT_RE)) {
|
|
386
410
|
const parts = shellWords(match[1] ?? "").map(cleanShellWord);
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
411
|
+
// Every `of=` counts: GNU dd honors the last one, so `of=/dev/null of=real.ts` writes real.ts.
|
|
412
|
+
const outputs = parts.filter(part => part.startsWith("of="));
|
|
413
|
+
if (outputs.length === 0) targets.unknown = true;
|
|
414
|
+
for (const output of outputs) {
|
|
415
|
+
const outputPath = cleanShellWord(output.slice(3));
|
|
416
|
+
if (!outputPath) targets.unknown = true;
|
|
417
|
+
else if (!isDeviceSinkPath(outputPath)) addPath(targets, outputPath);
|
|
418
|
+
}
|
|
390
419
|
}
|
|
391
420
|
for (const match of command.matchAll(BASH_IN_PLACE_MUTATION_COMMAND_RE)) {
|
|
392
421
|
const parts = shellWords(match[1] ?? "").map(cleanShellWord);
|
|
@@ -399,12 +428,17 @@ function extractBashTargets(args: unknown): ExtractedTargets {
|
|
|
399
428
|
for (const match of command.matchAll(BASH_MUTATION_COMMAND_RE)) {
|
|
400
429
|
const redirected = match[2]?.trim();
|
|
401
430
|
if (redirected) {
|
|
402
|
-
|
|
431
|
+
const cleaned = cleanShellWord(redirected);
|
|
432
|
+
// A capture that dequotes to nothing (e.g. `>" src.ts"`) is a truncated parse, not a safe no-op.
|
|
433
|
+
if (!cleaned) targets.unknown = true;
|
|
434
|
+
else if (!isDeviceSinkPath(cleaned)) addPath(targets, cleaned);
|
|
403
435
|
continue;
|
|
404
436
|
}
|
|
405
437
|
const parts = shellWords(match[1] ?? "");
|
|
406
438
|
const commandName = match[0]
|
|
407
|
-
?.match(
|
|
439
|
+
?.match(
|
|
440
|
+
/(?:^|[;&|\n])\s*(?:\w+=[^\s]+\s+)*(?:sudo\s+)?(?:[^\s;&|]*\/)?(tee|touch|rm|mkdir|cp|mv|install|truncate)\b/i,
|
|
441
|
+
)?.[1]
|
|
408
442
|
?.toLowerCase();
|
|
409
443
|
const targetParts =
|
|
410
444
|
commandName === "cp" || commandName === "mv" || commandName === "install" || commandName === "truncate"
|