@gajae-code/coding-agent 0.18.11 → 0.18.12
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 +293 -0
- package/dist/types/async/job-manager.d.ts +7 -3
- package/dist/types/async/result-formatting.d.ts +2 -0
- package/dist/types/autoresearch/git.d.ts +1 -1
- package/dist/types/capability/fs.d.ts +2 -2
- package/dist/types/cli/accounts-cli.d.ts +2 -2
- package/dist/types/cli/doctor/install-repairs.d.ts +4 -1
- package/dist/types/cli/install-activation.d.ts +10 -4
- package/dist/types/cli/public-command-errors.d.ts +1 -0
- package/dist/types/cli/update-cli.d.ts +14 -1
- package/dist/types/cli-ordinary.d.ts +2 -2
- package/dist/types/commands/accounts.d.ts +2 -1
- package/dist/types/commands/migrate.d.ts +1 -1
- package/dist/types/commands/sdk.d.ts +5 -0
- package/dist/types/commit/changelog/index.d.ts +1 -0
- package/dist/types/config/autorouting-tier-map.d.ts +30 -0
- package/dist/types/config/config-hot-reload-trust.d.ts +12 -0
- package/dist/types/config/config-hot-reload.d.ts +39 -0
- package/dist/types/config/file-lock.d.ts +8 -1
- package/dist/types/config/model-discovery-manager.d.ts +2 -0
- package/dist/types/config/model-profile-activation.d.ts +62 -6
- package/dist/types/config/model-registry.d.ts +56 -5
- package/dist/types/config/model-resolver.d.ts +2 -0
- package/dist/types/config/models-config-schema.d.ts +10 -0
- package/dist/types/config/settings-schema.d.ts +46 -3
- package/dist/types/config/settings.d.ts +33 -1
- package/dist/types/dap/config.d.ts +8 -4
- package/dist/types/debug/system-info.d.ts +6 -1
- package/dist/types/discovery/mcp-compat.d.ts +3 -1
- package/dist/types/edit/modes/patch.d.ts +5 -0
- package/dist/types/edit/modes/replace.d.ts +3 -0
- package/dist/types/exec/bash-executor.d.ts +12 -0
- package/dist/types/gjc-runtime/managed-owner-admission.d.ts +3 -0
- package/dist/types/gjc-runtime/session-state-lock.d.ts +2 -0
- package/dist/types/gjc-runtime/state-runtime.d.ts +0 -2
- package/dist/types/migrate/adapters/codex.d.ts +2 -1
- package/dist/types/migrate/adapters/index.d.ts +10 -1
- package/dist/types/migrate/executor.d.ts +3 -1
- package/dist/types/migrate/types.d.ts +15 -3
- package/dist/types/modes/components/settings-selector.d.ts +6 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +2 -0
- package/dist/types/modes/theme/theme.d.ts +1 -0
- package/dist/types/runtime-mcp/content-limits.d.ts +4 -0
- package/dist/types/runtime-mcp/manager.d.ts +7 -0
- package/dist/types/runtime-mcp/plugin-mcp-name-filter.d.ts +22 -0
- package/dist/types/runtime-mcp/tool-bridge.d.ts +4 -0
- package/dist/types/sdk/broker/broker-exit.d.ts +2 -2
- package/dist/types/sdk/broker/broker.d.ts +7 -1
- package/dist/types/sdk/broker/daemon-entry.d.ts +11 -1
- package/dist/types/sdk/broker/doctor-restart.d.ts +1 -1
- package/dist/types/sdk/broker/ensure.d.ts +136 -7
- package/dist/types/sdk/broker/hop.d.ts +36 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +18 -10
- package/dist/types/sdk/broker/lock-host-identity.d.ts +5 -0
- package/dist/types/sdk/broker/session-index.d.ts +4 -1
- package/dist/types/sdk/bus/chat-command-policy.d.ts +2 -1
- package/dist/types/sdk/bus/index.d.ts +23 -0
- package/dist/types/sdk/bus/reconciliation-store.d.ts +10 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +3 -1
- package/dist/types/sdk/cli/session-cli.d.ts +0 -1
- package/dist/types/sdk/host/session-runtime.d.ts +32 -0
- package/dist/types/sdk/prompt-deadline-flush.d.ts +42 -4
- package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -0
- package/dist/types/sdk/protocol/adapter-validation.d.ts +10 -4
- package/dist/types/sdk/reconciliation-extensions.d.ts +1 -1
- package/dist/types/sdk/session-list.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +58 -2
- package/dist/types/session/internal/managed-session-scope.d.ts +2 -2
- package/dist/types/session/internal/managed-session-storage.d.ts +1 -1
- package/dist/types/session/internal/native-publish-outcome.d.ts +1 -1
- package/dist/types/session/messages.d.ts +5 -1
- package/dist/types/session/session-manager.d.ts +13 -3
- package/dist/types/session/startup-auth-config.d.ts +24 -2
- package/dist/types/session/streaming-output.d.ts +11 -0
- package/dist/types/session/terminal-abort.d.ts +3 -1
- package/dist/types/session-import/service.d.ts +1 -1
- package/dist/types/skill-state/workflow-mutation-guard.d.ts +7 -3
- package/dist/types/slash-commands/helpers/provider-auth-status.d.ts +8 -0
- package/dist/types/task/executor.d.ts +19 -2
- package/dist/types/tools/ast-edit-write-path.d.ts +10 -0
- package/dist/types/tools/atomic-file-write.d.ts +9 -7
- package/dist/types/tools/bash-allowed-prefixes.d.ts +11 -0
- package/dist/types/tools/descriptors.d.ts +2 -0
- package/dist/types/tools/fetch.d.ts +1 -1
- package/dist/types/tools/index.d.ts +2 -0
- package/dist/types/tools/python.d.ts +1 -1
- package/dist/types/tools/report-issue.d.ts +41 -0
- package/dist/types/tools/subagent.d.ts +23 -0
- package/dist/types/web/scrapers/types.d.ts +8 -1
- package/dist/types/web/search/providers/insane.d.ts +7 -1
- package/dist/types/web/search/providers/searxng.d.ts +5 -0
- package/package.json +8 -8
- package/scripts/build-binary.ts +2 -1
- package/scripts/capture-subagent-compact-panel-showcase.ts +105 -0
- package/scripts/generate-docs-index.ts +8 -1
- package/scripts/generate-sdk-operation-inventory.ts +22 -4
- package/src/async/job-manager.ts +356 -56
- package/src/async/result-formatting.ts +40 -0
- package/src/autoresearch/git.ts +9 -2
- package/src/autoresearch/prompts/prompt-setup.md +1 -1
- package/src/capability/fs.ts +27 -19
- package/src/capability/index.ts +2 -2
- package/src/cli/accounts-cli.ts +176 -8
- package/src/cli/auth-broker-cli.ts +21 -11
- package/src/cli/doctor/install-repairs.ts +35 -5
- package/src/cli/fast-help.ts +1 -1
- package/src/cli/install-activation.ts +47 -4
- package/src/cli/migrate-cli.ts +7 -5
- package/src/cli/public-command-errors.ts +4 -0
- package/src/cli/root-flags.ts +2 -2
- package/src/cli/stats-cli.ts +9 -5
- package/src/cli/update-cli.ts +224 -7
- package/src/cli-ordinary.ts +7 -2
- package/src/commands/accounts.ts +10 -5
- package/src/commands/migrate.ts +3 -3
- package/src/commands/sdk.ts +199 -42
- package/src/commands/ultragoal.ts +16 -8
- package/src/commit/changelog/index.ts +39 -1
- package/src/commit/message.ts +10 -3
- package/src/config/autorouting-tier-map.ts +30 -0
- package/src/config/config-hot-reload-trust.ts +148 -0
- package/src/config/config-hot-reload.ts +734 -0
- package/src/config/file-lock.ts +216 -56
- package/src/config/model-bindings-applier.ts +85 -47
- package/src/config/model-discovery-manager.ts +10 -0
- package/src/config/model-profile-activation.ts +716 -113
- package/src/config/model-registry.ts +1334 -158
- package/src/config/model-resolver.ts +3 -1
- package/src/config/models-config-schema.ts +6 -0
- package/src/config/settings-schema.ts +60 -3
- package/src/config/settings.ts +458 -24
- package/src/coordinator-mcp/policy.ts +26 -1
- package/src/crash/sanitize.ts +4 -0
- package/src/customization/import.ts +3 -1
- package/src/dap/config.ts +31 -11
- package/src/dap/defaults.json +1 -1
- package/src/debug/crash-diagnostics.ts +2 -1
- package/src/debug/report-bundle.ts +19 -0
- package/src/debug/system-info.ts +8 -3
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.ts +21 -7
- package/src/discovery/agents-md.ts +1 -1
- package/src/discovery/helpers.ts +1 -2
- package/src/discovery/mcp-compat.ts +120 -17
- package/src/edit/apply-patch/index.ts +12 -1
- package/src/edit/index.ts +13 -17
- package/src/edit/modes/patch.ts +187 -8
- package/src/edit/modes/replace.ts +136 -0
- package/src/eval/js/context-manager.ts +29 -3
- package/src/eval/js/worker-entry.ts +51 -18
- package/src/eval/py/executor.ts +14 -4
- package/src/eval/py/runner.py +144 -5
- package/src/exec/bash-executor.ts +81 -4
- package/src/exec/bash-shell-guardian.ts +4 -1
- package/src/exec/bash-shell-supervisor.ts +8 -4
- package/src/exec/isolated-shell.ts +5 -1
- package/src/export/html/index.ts +46 -6
- package/src/extensibility/custom-tools/wrapper.ts +3 -1
- package/src/extensibility/extensions/loader.ts +8 -1
- package/src/extensibility/plugins/marketplace/fetcher.ts +2 -2
- package/src/gjc-runtime/gc-runtime.ts +33 -6
- package/src/gjc-runtime/launch-tmux.ts +7 -2
- package/src/gjc-runtime/launch-worktree.test.ts +148 -0
- package/src/gjc-runtime/launch-worktree.ts +101 -14
- package/src/gjc-runtime/managed-owner-admission.ts +46 -2
- package/src/gjc-runtime/session-resolution.ts +38 -6
- package/src/gjc-runtime/session-state-lock.ts +59 -14
- package/src/gjc-runtime/state-runtime.ts +23 -19
- package/src/hooks/native-skill-hook.ts +14 -11
- package/src/internal-urls/docs-index.generated.ts +14 -13
- package/src/internal-urls/local-protocol.ts +142 -8
- package/src/lsp/index.ts +29 -0
- package/src/lsp/workspace-path.ts +23 -0
- package/src/migrate/action-planner.ts +57 -0
- package/src/migrate/adapters/claude-code.ts +23 -4
- package/src/migrate/adapters/codex.ts +22 -4
- package/src/migrate/adapters/index.ts +22 -4
- package/src/migrate/adapters/opencode.ts +6 -1
- package/src/migrate/executor.ts +84 -7
- package/src/migrate/report.ts +19 -2
- package/src/migrate/types.ts +14 -4
- package/src/modes/DESIGN.md +51 -0
- package/src/modes/acp/permission-mode.ts +18 -1
- package/src/modes/components/assistant-message.ts +55 -1
- package/src/modes/components/settings-selector.ts +103 -37
- package/src/modes/controllers/command-controller.ts +38 -2
- package/src/modes/controllers/plan-mode-controller.ts +17 -7
- package/src/modes/controllers/selector-controller.ts +62 -6
- package/src/modes/interactive-mode.ts +68 -0
- package/src/modes/theme/theme.ts +15 -0
- package/src/prompts/tools/debug.md +1 -1
- package/src/prompts/tools/read.md +1 -1
- package/src/prompts/tools/report-issue.md +13 -0
- package/src/prompts/tools/task.md +1 -1
- package/src/runtime-credential-selector.ts +2 -2
- package/src/runtime-mcp/content-limits.ts +7 -0
- package/src/runtime-mcp/manager-pool.test.ts +5 -4
- package/src/runtime-mcp/manager.ts +32 -0
- package/src/runtime-mcp/oauth-flow.ts +32 -6
- package/src/runtime-mcp/plugin-mcp-name-filter.ts +62 -0
- package/src/runtime-mcp/render.ts +16 -1
- package/src/runtime-mcp/tool-bridge.ts +126 -17
- package/src/sdk/broker/broker-exit.ts +8 -5
- package/src/sdk/broker/broker.ts +342 -82
- package/src/sdk/broker/daemon-entry.ts +338 -116
- package/src/sdk/broker/doctor-restart.ts +2 -1
- package/src/sdk/broker/ensure.ts +1143 -265
- package/src/sdk/broker/hop.ts +274 -0
- package/src/sdk/broker/lifecycle-ledger.ts +35 -11
- package/src/sdk/broker/lock-host-identity.ts +29 -0
- package/src/sdk/broker/session-index.ts +62 -12
- package/src/sdk/bus/chat-command-policy.ts +16 -3
- package/src/sdk/bus/index.ts +52 -21
- package/src/sdk/bus/reconciliation-store.ts +201 -8
- package/src/sdk/bus/telegram-daemon-contract.ts +3 -1
- package/src/sdk/cli/session-cli.ts +21 -6
- package/src/sdk/host/session-runtime.test.ts +118 -23
- package/src/sdk/host/session-runtime.ts +352 -47
- package/src/sdk/lifecycle/service.ts +1 -0
- package/src/sdk/prompt-deadline-flush.ts +78 -5
- package/src/sdk/prompt-deadline-manager.ts +68 -14
- package/src/sdk/protocol/adapter-validation.ts +53 -11
- package/src/sdk/protocol/operation-inventory.generated.json +101 -2
- package/src/sdk/reconciliation-extensions.ts +1 -0
- package/src/sdk/session-list.ts +1 -0
- package/src/sdk/session.ts +114 -61
- package/src/session/agent-session.ts +1043 -81
- package/src/session/internal/managed-session-scope.ts +126 -39
- package/src/session/internal/managed-session-storage.ts +14 -11
- package/src/session/internal/native-publish-outcome.ts +5 -1
- package/src/session/messages.ts +47 -6
- package/src/session/session-manager.ts +236 -14
- package/src/session/startup-auth-config.ts +116 -8
- package/src/session/streaming-output.ts +262 -4
- package/src/session/terminal-abort.ts +14 -2
- package/src/session-import/service.ts +1 -0
- package/src/skill-state/workflow-mutation-guard.ts +491 -32
- package/src/slash-commands/builtin-registry.ts +15 -3
- package/src/slash-commands/helpers/credential-switch.ts +10 -5
- package/src/slash-commands/helpers/provider-auth-status.ts +456 -0
- package/src/task/executor.ts +103 -12
- package/src/task/index.ts +389 -175
- package/src/tools/ast-edit-write-path.ts +64 -0
- package/src/tools/ast-edit.ts +18 -1
- package/src/tools/atomic-file-write.ts +46 -15
- package/src/tools/auto-generated-guard.ts +3 -0
- package/src/tools/bash-allowed-prefixes.ts +153 -5
- package/src/tools/bash-interactive.ts +14 -2
- package/src/tools/bash.ts +14 -0
- package/src/tools/debug.ts +7 -6
- package/src/tools/descriptors.test.ts +44 -0
- package/src/tools/descriptors.ts +34 -2
- package/src/tools/fetch.ts +3 -2
- package/src/tools/index.ts +2 -0
- package/src/tools/output-meta.ts +23 -2
- package/src/tools/python.ts +1 -1
- package/src/tools/read.ts +17 -35
- package/src/tools/report-issue.ts +184 -0
- package/src/tools/subagent-render.ts +114 -12
- package/src/tools/subagent.ts +108 -1
- package/src/tools/todo-write.ts +41 -3
- package/src/tools/tool-catalog.generated.ts +31 -3
- package/src/tools/write.ts +35 -3
- package/src/utils/commit-message-generator.ts +9 -2
- package/src/utils/file-mentions.ts +29 -1
- package/src/utils/title-generator.ts +7 -2
- package/src/web/scrapers/index.ts +3 -1
- package/src/web/scrapers/types.ts +8 -0
- package/src/web/scrapers/youtube.ts +3 -2
- package/src/web/search/providers/insane.ts +91 -28
- package/src/web/search/providers/searxng.ts +62 -10
|
@@ -3,9 +3,14 @@ import * as os from "node:os";
|
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import type { AgentTool } from "@gajae-code/agent-core";
|
|
5
5
|
import { logger } from "@gajae-code/utils";
|
|
6
|
+
import { canonicalEnvKey, type ProjectEnvSnapshot, projectEnvSnapshot } from "@gajae-code/utils/env-file";
|
|
6
7
|
import { isAutoresearchAuthorizedResearchPath } from "../autoresearch/git";
|
|
7
8
|
import { expandApplyPatchToEntries } from "../edit/modes/apply-patch";
|
|
8
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
GJC_SESSION_PREFIX,
|
|
11
|
+
sessionDirName,
|
|
12
|
+
modeStatePath as sessionModeStatePath,
|
|
13
|
+
} from "../gjc-runtime/session-layout";
|
|
9
14
|
import { resolveGjcSessionForRead } from "../gjc-runtime/session-resolution";
|
|
10
15
|
import { ModeStateSchema } from "../gjc-runtime/state-schema";
|
|
11
16
|
import { workflowEnvelopeChecksumStatus } from "../gjc-runtime/state-writer";
|
|
@@ -30,7 +35,7 @@ export const RALPLAN_MUTATION_BLOCK_MESSAGE =
|
|
|
30
35
|
export const ULTRAGOAL_GOAL_PLANNING_MUTATION_BLOCK_MESSAGE =
|
|
31
36
|
"Ultragoal goal-planning phase boundary: finish goal planning and record goals through `gjc ultragoal` before editing code. Product-code mutation tools and patch execution are blocked until goal planning completes and execution begins.";
|
|
32
37
|
export const AUTORESEARCH_MUTATION_BLOCK_MESSAGE =
|
|
33
|
-
"Autoresearch research-only boundary: this workflow produces findings, evidence, and a verdict — never product code. Product-code mutation tools and patch execution are blocked everywhere, on every branch.
|
|
38
|
+
"Autoresearch research-only boundary: this workflow produces findings, evidence, and a verdict — never product code. Product-code mutation tools and patch execution are blocked everywhere, on every branch. Write the root `autoresearch.sh` only with direct file tools; mission state stays writable through the sanctioned `gjc autoresearch` CLI. Run experiments through the `python` tool, or finish the mission and hand off to an approved implementation workflow.";
|
|
34
39
|
|
|
35
40
|
/** Resolve the phase-boundary block message for the active planning skill. */
|
|
36
41
|
function planningPhaseBlockMessage(skill: CanonicalGjcWorkflowSkill): string {
|
|
@@ -40,10 +45,15 @@ function planningPhaseBlockMessage(skill: CanonicalGjcWorkflowSkill): string {
|
|
|
40
45
|
return DEEP_INTERVIEW_MUTATION_BLOCK_MESSAGE;
|
|
41
46
|
}
|
|
42
47
|
|
|
43
|
-
/**
|
|
44
|
-
|
|
48
|
+
/**
|
|
49
|
+
* Tools whose execute path can mutate product state. `github` is included so
|
|
50
|
+
* side-effect ops share this guard. `eval` and `python` run code that can write
|
|
51
|
+
* files without going through edit, write, or bash.
|
|
52
|
+
*/
|
|
53
|
+
export const WORKFLOW_MUTATION_TOOL_NAMES = ["edit", "write", "ast_edit", "bash", "github", "eval", "python"] as const;
|
|
45
54
|
|
|
46
55
|
const BLOCKED_TOOL_NAMES = new Set<string>(WORKFLOW_MUTATION_TOOL_NAMES);
|
|
56
|
+
const AUTORESEARCH_HARNESS_FILE_TOOL_NAMES = new Set(["edit", "write"]);
|
|
47
57
|
|
|
48
58
|
export function isWorkflowMutationTool(name: string): boolean {
|
|
49
59
|
return BLOCKED_TOOL_NAMES.has(name);
|
|
@@ -79,14 +89,15 @@ const BASH_COMMAND_WRAPPER_SOURCE =
|
|
|
79
89
|
*/
|
|
80
90
|
const BASH_COMMAND_POSITION_KEYWORD_RE = /(?:^|[;&|(\n{}])\s*(?:then|else|elif|do|in|\{|!)(?=\s)/gi;
|
|
81
91
|
const BASH_MUTATION_COMMAND_RE = new RegExp(
|
|
82
|
-
`(?:^|[;&|(\\n])\\s*(?:\\w+=[^\\s]+\\s+)*(?:sudo\\s+)?${BASH_COMMAND_WRAPPER_SOURCE}(?:[^\\s;&|()]*\\/)?(?:tee|touch|rm|mkdir|cp|mv|install|truncate)\\b([^;&|()\\n]*)|(?:^|[^<>])(?:>>?|\\d>>?)\\s*([^\\s;&|]+)`,
|
|
92
|
+
`(?:^|[;&|(\\n])\\s*(?:\\w+=[^\\s]+\\s+)*(?:sudo\\s+)?${BASH_COMMAND_WRAPPER_SOURCE}(?:[^\\s;&|()]*\\/)?(?:tee|touch|rm|mkdir|cp|mv|install|truncate|chmod|ln)\\b([^;&|()\\n]*)|(?:^|[^<>])(?:>>?|\\d>>?)\\s*([^\\s;&|]+)`,
|
|
83
93
|
"gi",
|
|
84
94
|
);
|
|
85
95
|
/** Command-word extractor; must accept the same prefixes as `BASH_MUTATION_COMMAND_RE`. */
|
|
86
96
|
const BASH_MUTATION_COMMAND_NAME_RE = new RegExp(
|
|
87
|
-
`(?:^|[;&|(\\n])\\s*(?:\\w+=[^\\s]+\\s+)*(?:sudo\\s+)?${BASH_COMMAND_WRAPPER_SOURCE}(?:[^\\s;&|()]*\\/)?(tee|touch|rm|mkdir|cp|mv|install|truncate)\\b`,
|
|
97
|
+
`(?:^|[;&|(\\n])\\s*(?:\\w+=[^\\s]+\\s+)*(?:sudo\\s+)?${BASH_COMMAND_WRAPPER_SOURCE}(?:[^\\s;&|()]*\\/)?(tee|touch|rm|mkdir|cp|mv|install|truncate|chmod|ln)\\b`,
|
|
88
98
|
"i",
|
|
89
99
|
);
|
|
100
|
+
const BASH_CHMOD_MODE_RE = /^(?:[0-7]{1,4}|(?:[ugoa]*[+=-][rwxXstugo]*)(?:,[ugoa]*[+=-][rwxXstugo]*)*)$/;
|
|
90
101
|
const BASH_IN_PLACE_MUTATION_COMMAND_RE = new RegExp(
|
|
91
102
|
`(?:^|[;&|(\\n])\\s*(?:\\w+=[^\\s]+\\s+)*(?:sudo\\s+)?${BASH_COMMAND_WRAPPER_SOURCE}(?:[^\\s;&|()]*\\/)?(?:sed|perl)\\b([^;&|()\\n]*)`,
|
|
92
103
|
"gi",
|
|
@@ -1667,10 +1678,20 @@ function extractBashTargets(args: unknown, depth = 0): ExtractedTargets {
|
|
|
1667
1678
|
targets.explicitMutation = true;
|
|
1668
1679
|
const parts = shellWords(match[1] ?? "");
|
|
1669
1680
|
const commandName = match[0]?.match(BASH_MUTATION_COMMAND_NAME_RE)?.[1]?.toLowerCase();
|
|
1670
|
-
const
|
|
1671
|
-
commandName === "
|
|
1672
|
-
|
|
1673
|
-
|
|
1681
|
+
const chmodModeIndex =
|
|
1682
|
+
commandName === "chmod" ? parts.findIndex(part => BASH_CHMOD_MODE_RE.test(cleanShellWord(part))) : -1;
|
|
1683
|
+
let targetParts = parts;
|
|
1684
|
+
if (
|
|
1685
|
+
commandName === "cp" ||
|
|
1686
|
+
commandName === "mv" ||
|
|
1687
|
+
commandName === "install" ||
|
|
1688
|
+
commandName === "truncate" ||
|
|
1689
|
+
commandName === "ln"
|
|
1690
|
+
) {
|
|
1691
|
+
targetParts = parts.slice(-1);
|
|
1692
|
+
} else if (commandName === "chmod" && chmodModeIndex >= 0) {
|
|
1693
|
+
targetParts = parts.filter((_part, index) => index !== chmodModeIndex);
|
|
1694
|
+
}
|
|
1674
1695
|
let extractedTarget = false;
|
|
1675
1696
|
for (const part of targetParts) {
|
|
1676
1697
|
const cleaned = cleanShellWord(part);
|
|
@@ -1698,12 +1719,315 @@ function extractGithubTargets(args: unknown): ExtractedTargets {
|
|
|
1698
1719
|
return { paths: [], unknown: true, explicitMutation: true };
|
|
1699
1720
|
}
|
|
1700
1721
|
|
|
1722
|
+
function simpleLiteral(raw: string): string | null {
|
|
1723
|
+
const trimmed = raw.trim();
|
|
1724
|
+
const quote = trimmed[0];
|
|
1725
|
+
if ((quote !== "'" && quote !== '"') || trimmed.length < 2 || trimmed[trimmed.length - 1] !== quote) return null;
|
|
1726
|
+
const body = trimmed.slice(1, -1);
|
|
1727
|
+
return /[\\\n${]/.test(body) || body.includes(quote) ? null : body;
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
function skipNoise(code: string, index: number, language: "py" | "js"): number {
|
|
1731
|
+
if (language === "py" && (code.startsWith('"""', index) || code.startsWith("'''", index))) {
|
|
1732
|
+
const quote = code.slice(index, index + 3);
|
|
1733
|
+
const end = code.indexOf(quote, index + 3);
|
|
1734
|
+
return end < 0 ? code.length : end + 3;
|
|
1735
|
+
}
|
|
1736
|
+
const character = code[index] ?? "";
|
|
1737
|
+
if (character === "'" || character === '"') {
|
|
1738
|
+
let cursor = index + 1;
|
|
1739
|
+
while (cursor < code.length && code[cursor] !== character) cursor += code[cursor] === "\\" ? 2 : 1;
|
|
1740
|
+
return Math.min(code.length, cursor + 1);
|
|
1741
|
+
}
|
|
1742
|
+
if (
|
|
1743
|
+
(language === "py" && character === "#") ||
|
|
1744
|
+
(language === "js" && character === "/" && code[index + 1] === "/")
|
|
1745
|
+
) {
|
|
1746
|
+
const end = code.indexOf("\n", index);
|
|
1747
|
+
return end < 0 ? code.length : end;
|
|
1748
|
+
}
|
|
1749
|
+
if (language === "js" && character === "/" && code[index + 1] === "*") {
|
|
1750
|
+
const end = code.indexOf("*/", index + 2);
|
|
1751
|
+
return end < 0 ? code.length : end + 2;
|
|
1752
|
+
}
|
|
1753
|
+
return index;
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
function readArg(code: string, index: number): { raw: string; end: number } | null {
|
|
1757
|
+
let cursor = index;
|
|
1758
|
+
while (/\s/.test(code[cursor] ?? "")) cursor += 1;
|
|
1759
|
+
if (cursor >= code.length || code[cursor] === ")") return null;
|
|
1760
|
+
const start = cursor;
|
|
1761
|
+
let depth = 0;
|
|
1762
|
+
while (cursor < code.length) {
|
|
1763
|
+
const character = code[cursor];
|
|
1764
|
+
if (character === "(") depth += 1;
|
|
1765
|
+
else if ((character === "," || character === ")") && depth === 0) break;
|
|
1766
|
+
else if (character === ")") depth -= 1;
|
|
1767
|
+
else if (character === "'" || character === '"') {
|
|
1768
|
+
const quote = character;
|
|
1769
|
+
cursor += 1;
|
|
1770
|
+
while (cursor < code.length && code[cursor] !== quote) cursor += code[cursor] === "\\" ? 2 : 1;
|
|
1771
|
+
}
|
|
1772
|
+
cursor += 1;
|
|
1773
|
+
}
|
|
1774
|
+
const raw = code.slice(start, cursor).trim();
|
|
1775
|
+
return raw.length > 0 ? { raw, end: cursor } : null;
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
/** Omitted mode is Python's default `"r"`. A non-literal mode cannot be proved read-only. */
|
|
1779
|
+
function modeWrites(raw: string | undefined): boolean | null {
|
|
1780
|
+
if (raw === undefined) return false;
|
|
1781
|
+
const literal = simpleLiteral(raw);
|
|
1782
|
+
return literal === null ? null : /[wax+]/.test(literal);
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
const PY_OS_MUTATORS = new Set("remove unlink rename replace rmtree move copy copy2 copyfile".split(" "));
|
|
1786
|
+
/** Copy and rename write the destination, not only the source. */
|
|
1787
|
+
const TWO_ARG_MUTATORS = new Set("rename replace move copy copy2 copyfile renameSync copyFileSync".split(" "));
|
|
1788
|
+
const JS_FS_MUTATORS = new Set(
|
|
1789
|
+
"writeFileSync writeFile appendFileSync appendFile rmSync unlinkSync renameSync copyFileSync mkdirSync openSync".split(
|
|
1790
|
+
" ",
|
|
1791
|
+
),
|
|
1792
|
+
);
|
|
1793
|
+
|
|
1794
|
+
function nextCallArg(code: string, end: number): { raw: string; end: number } | null {
|
|
1795
|
+
let cursor = end;
|
|
1796
|
+
while (/\s/.test(code[cursor] ?? "")) cursor += 1;
|
|
1797
|
+
if (code[cursor] !== ",") return null;
|
|
1798
|
+
return readArg(code, cursor + 1);
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
function openModeWrites(code: string, first: { raw: string; end: number }): boolean | null {
|
|
1802
|
+
let cursor = first.end;
|
|
1803
|
+
let positionalIndex = 1;
|
|
1804
|
+
let mode: string | undefined;
|
|
1805
|
+
let unknownMode = false;
|
|
1806
|
+
while (cursor < code.length) {
|
|
1807
|
+
while (/\s/.test(code[cursor] ?? "")) cursor += 1;
|
|
1808
|
+
if (code[cursor] !== ",") break;
|
|
1809
|
+
const argument = readArg(code, cursor + 1);
|
|
1810
|
+
if (!argument) break;
|
|
1811
|
+
const raw = argument.raw.trim();
|
|
1812
|
+
if (raw.startsWith("*") || raw.startsWith("**")) {
|
|
1813
|
+
unknownMode = true;
|
|
1814
|
+
} else {
|
|
1815
|
+
const keyword = /^([A-Za-z_]\w*)\s*=\s*([\s\S]*)$/.exec(raw);
|
|
1816
|
+
if (keyword?.[1] === "mode") {
|
|
1817
|
+
if (mode !== undefined) unknownMode = true;
|
|
1818
|
+
else mode = keyword[2];
|
|
1819
|
+
} else if (!keyword && positionalIndex === 1) {
|
|
1820
|
+
if (mode !== undefined) unknownMode = true;
|
|
1821
|
+
else mode = raw;
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
positionalIndex += 1;
|
|
1825
|
+
cursor = argument.end;
|
|
1826
|
+
}
|
|
1827
|
+
if (unknownMode) return null;
|
|
1828
|
+
return modeWrites(mode);
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
/** Whitespace or a backslash-newline may sit before an attribute (`shutil .copy`). */
|
|
1832
|
+
function skipCallGap(code: string, index: number): number {
|
|
1833
|
+
let cursor = index;
|
|
1834
|
+
while (cursor < code.length) {
|
|
1835
|
+
const character = code[cursor] ?? "";
|
|
1836
|
+
if (/\s/.test(character)) {
|
|
1837
|
+
cursor += 1;
|
|
1838
|
+
continue;
|
|
1839
|
+
}
|
|
1840
|
+
if (character === "\\" && code[cursor + 1] === "\n") {
|
|
1841
|
+
cursor += 2;
|
|
1842
|
+
continue;
|
|
1843
|
+
}
|
|
1844
|
+
if (character === "\\" && code[cursor + 1] === "\r" && code[cursor + 2] === "\n") {
|
|
1845
|
+
cursor += 3;
|
|
1846
|
+
continue;
|
|
1847
|
+
}
|
|
1848
|
+
break;
|
|
1849
|
+
}
|
|
1850
|
+
return cursor;
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
function literalWrite(targets: ExtractedTargets, raw: string | undefined, write: boolean | null): void {
|
|
1854
|
+
if (write === false) return;
|
|
1855
|
+
targets.explicitMutation = true;
|
|
1856
|
+
const literal = raw === undefined ? null : simpleLiteral(raw);
|
|
1857
|
+
if (write === null || literal === null || literal.length === 0) {
|
|
1858
|
+
targets.unknown = true;
|
|
1859
|
+
return;
|
|
1860
|
+
}
|
|
1861
|
+
addPath(targets, literal);
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
/** Arguments of one Path() call, through its own closing parenthesis. */
|
|
1865
|
+
function readPathConstructor(code: string, argAt: number): { parts: string[]; after: number } | null {
|
|
1866
|
+
let cursor = argAt;
|
|
1867
|
+
const parts: string[] = [];
|
|
1868
|
+
while (cursor < code.length) {
|
|
1869
|
+
while (/\s/.test(code[cursor] ?? "")) cursor += 1;
|
|
1870
|
+
if (code[cursor] === ")") return { parts, after: cursor + 1 };
|
|
1871
|
+
const argument = readArg(code, cursor);
|
|
1872
|
+
if (!argument) return null;
|
|
1873
|
+
parts.push(argument.raw);
|
|
1874
|
+
cursor = argument.end;
|
|
1875
|
+
while (/\s/.test(code[cursor] ?? "")) cursor += 1;
|
|
1876
|
+
if (code[cursor] === ",") {
|
|
1877
|
+
cursor += 1;
|
|
1878
|
+
continue;
|
|
1879
|
+
}
|
|
1880
|
+
if (code[cursor] === ")") return { parts, after: cursor + 1 };
|
|
1881
|
+
return null;
|
|
1882
|
+
}
|
|
1883
|
+
return null;
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
/** pathlib keeps the last absolute segment. A non-literal segment is an unresolved receiver. */
|
|
1887
|
+
function literalPathReceiver(parts: string[]): string | null {
|
|
1888
|
+
if (parts.length === 0) return ".";
|
|
1889
|
+
let joined = "";
|
|
1890
|
+
for (const part of parts) {
|
|
1891
|
+
const literal = simpleLiteral(part);
|
|
1892
|
+
if (literal === null) return null;
|
|
1893
|
+
if (path.isAbsolute(literal)) joined = literal;
|
|
1894
|
+
else joined = joined ? path.join(joined, literal) : literal;
|
|
1895
|
+
}
|
|
1896
|
+
return joined.length === 0 ? "." : joined;
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
/** Calls inside strings or comments are skipped. A non-literal write path fails closed. */
|
|
1900
|
+
function scanInterpreterWrites(code: string, language: "py" | "js", targets: ExtractedTargets): void {
|
|
1901
|
+
let index = 0;
|
|
1902
|
+
while (index < code.length) {
|
|
1903
|
+
const trivia = skipNoise(code, index, language);
|
|
1904
|
+
if (trivia !== index) {
|
|
1905
|
+
index = trivia;
|
|
1906
|
+
continue;
|
|
1907
|
+
}
|
|
1908
|
+
const previous = index === 0 ? "" : (code[index - 1] ?? "");
|
|
1909
|
+
if ((index > 0 && /[\w]/.test(previous)) || !/[A-Za-z_]/.test(code[index] ?? "")) {
|
|
1910
|
+
index += 1;
|
|
1911
|
+
continue;
|
|
1912
|
+
}
|
|
1913
|
+
const ident = /^[A-Za-z_]\w*/.exec(code.slice(index));
|
|
1914
|
+
if (!ident) {
|
|
1915
|
+
index += 1;
|
|
1916
|
+
continue;
|
|
1917
|
+
}
|
|
1918
|
+
const name = ident[0];
|
|
1919
|
+
if ((name === "write" || name === "append") && previous === ".") {
|
|
1920
|
+
index += name.length;
|
|
1921
|
+
continue;
|
|
1922
|
+
}
|
|
1923
|
+
let cursor = skipCallGap(code, index + name.length);
|
|
1924
|
+
let dotted = "";
|
|
1925
|
+
if (code[cursor] === ".") {
|
|
1926
|
+
const methodAt = skipCallGap(code, cursor + 1);
|
|
1927
|
+
const dot = /^\w+/.exec(code.slice(methodAt));
|
|
1928
|
+
dotted = dot?.[0] ?? "";
|
|
1929
|
+
cursor = methodAt + dotted.length;
|
|
1930
|
+
}
|
|
1931
|
+
const paren = /^\s*\(/.exec(code.slice(cursor));
|
|
1932
|
+
const helper = (name === "write" || name === "append") && dotted === "";
|
|
1933
|
+
const open = (name === "open" && language === "py") || (name === "openSync" && language === "js");
|
|
1934
|
+
const pathCtor = name === "Path" && dotted === "";
|
|
1935
|
+
const osCall =
|
|
1936
|
+
language === "py" &&
|
|
1937
|
+
((name === "os" && PY_OS_MUTATORS.has(dotted)) || (name === "shutil" && PY_OS_MUTATORS.has(dotted)));
|
|
1938
|
+
const jsCall =
|
|
1939
|
+
language === "js" && ((JS_FS_MUTATORS.has(name) && dotted === "") || (name === "Bun" && dotted === "write"));
|
|
1940
|
+
if (!paren || (!helper && !(open && dotted === "") && !pathCtor && !osCall && !jsCall)) {
|
|
1941
|
+
index += name.length;
|
|
1942
|
+
continue;
|
|
1943
|
+
}
|
|
1944
|
+
const argAt = cursor + paren[0].length;
|
|
1945
|
+
const first = readArg(code, argAt);
|
|
1946
|
+
if (pathCtor) {
|
|
1947
|
+
const ctor = readPathConstructor(code, argAt);
|
|
1948
|
+
if (!ctor) {
|
|
1949
|
+
index += 1;
|
|
1950
|
+
continue;
|
|
1951
|
+
}
|
|
1952
|
+
const method = /^\s*\.\s*(write_text|write_bytes)\b/.exec(code.slice(ctor.after));
|
|
1953
|
+
if (method) {
|
|
1954
|
+
targets.explicitMutation = true;
|
|
1955
|
+
const receiver = literalPathReceiver(ctor.parts);
|
|
1956
|
+
if (receiver === null || receiver.length === 0) targets.unknown = true;
|
|
1957
|
+
else addPath(targets, receiver);
|
|
1958
|
+
}
|
|
1959
|
+
// Extra segments stay in the argument walk so a nested mutator is still recorded.
|
|
1960
|
+
index =
|
|
1961
|
+
ctor.parts.length > 1 ? index + name.length : Math.max(ctor.after + (method?.[0].length ?? 0), index + 1);
|
|
1962
|
+
continue;
|
|
1963
|
+
}
|
|
1964
|
+
if (!first) {
|
|
1965
|
+
literalWrite(targets, undefined, null);
|
|
1966
|
+
index = Math.max(argAt, index + 1);
|
|
1967
|
+
continue;
|
|
1968
|
+
}
|
|
1969
|
+
if (open && dotted === "") {
|
|
1970
|
+
literalWrite(targets, first.raw, openModeWrites(code, first));
|
|
1971
|
+
index = Math.max(first.end, index + 1);
|
|
1972
|
+
continue;
|
|
1973
|
+
}
|
|
1974
|
+
literalWrite(targets, first.raw, true);
|
|
1975
|
+
let end = first.end;
|
|
1976
|
+
if (TWO_ARG_MUTATORS.has(dotted || name)) {
|
|
1977
|
+
const second = nextCallArg(code, first.end);
|
|
1978
|
+
if (!second) literalWrite(targets, undefined, null);
|
|
1979
|
+
else {
|
|
1980
|
+
literalWrite(targets, second.raw, true);
|
|
1981
|
+
end = second.end;
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
index = Math.max(end, index + 1);
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
function extractCodeToolTargets(toolName: string, args: unknown): ExtractedTargets {
|
|
1989
|
+
const targets: ExtractedTargets = { paths: [], unknown: false };
|
|
1990
|
+
const record = getRecord(args);
|
|
1991
|
+
if (!record) {
|
|
1992
|
+
targets.unknown = true;
|
|
1993
|
+
targets.explicitMutation = true;
|
|
1994
|
+
return targets;
|
|
1995
|
+
}
|
|
1996
|
+
if (toolName === "python") {
|
|
1997
|
+
if ((safeString(record.action) || "execute") === "clear") return targets;
|
|
1998
|
+
if (typeof record.code !== "string" || record.code.trim() === "") return targets;
|
|
1999
|
+
scanInterpreterWrites(record.code, "py", targets);
|
|
2000
|
+
return targets;
|
|
2001
|
+
}
|
|
2002
|
+
if (!Array.isArray(record.cells)) {
|
|
2003
|
+
targets.unknown = true;
|
|
2004
|
+
targets.explicitMutation = true;
|
|
2005
|
+
return targets;
|
|
2006
|
+
}
|
|
2007
|
+
for (const cell of record.cells) {
|
|
2008
|
+
const cellRecord = getRecord(cell);
|
|
2009
|
+
if (typeof cellRecord?.code !== "string") {
|
|
2010
|
+
targets.unknown = true;
|
|
2011
|
+
targets.explicitMutation = true;
|
|
2012
|
+
continue;
|
|
2013
|
+
}
|
|
2014
|
+
if (cellRecord.language === "py") scanInterpreterWrites(cellRecord.code, "py", targets);
|
|
2015
|
+
else if (cellRecord.language === "js") scanInterpreterWrites(cellRecord.code, "js", targets);
|
|
2016
|
+
else {
|
|
2017
|
+
targets.unknown = true;
|
|
2018
|
+
targets.explicitMutation = true;
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
return targets;
|
|
2022
|
+
}
|
|
2023
|
+
|
|
1701
2024
|
function extractTargets(tool: ToolWithEditMode, args: unknown): ExtractedTargets {
|
|
1702
2025
|
if (tool.name === "write") return extractWriteTargets(args);
|
|
1703
2026
|
if (tool.name === "ast_edit") return extractAstEditTargets(args);
|
|
1704
2027
|
if (tool.name === "edit") return extractEditTargets(args, tool);
|
|
1705
2028
|
if (tool.name === "bash") return extractBashTargets(args);
|
|
1706
2029
|
if (tool.name === "github") return extractGithubTargets(args);
|
|
2030
|
+
if (tool.name === "python" || tool.name === "eval") return extractCodeToolTargets(tool.name, args);
|
|
1707
2031
|
return { paths: [], unknown: true };
|
|
1708
2032
|
}
|
|
1709
2033
|
|
|
@@ -1766,11 +2090,23 @@ function firstBlockedWorkflowStateSkill(cwd: string, targets: ExtractedTargets):
|
|
|
1766
2090
|
return null;
|
|
1767
2091
|
}
|
|
1768
2092
|
|
|
1769
|
-
function
|
|
2093
|
+
function planningSessionDirName(sessionId: string | null | undefined): string | undefined {
|
|
2094
|
+
const trimmed = sessionId?.trim();
|
|
2095
|
+
if (!trimmed) return undefined;
|
|
2096
|
+
return sessionDirName(trimmed);
|
|
2097
|
+
}
|
|
2098
|
+
|
|
2099
|
+
/** Specs and plans are planning-writable. A `_session-` directory must be this session's encoded name. */
|
|
2100
|
+
function isAllowlistedPath(cwd: string, rawPath: string, sessionId?: string | null): boolean {
|
|
1770
2101
|
const segments = relativeGjcSegments(cwd, rawPath);
|
|
1771
2102
|
if (segments?.[0] !== ".gjc") return false;
|
|
1772
|
-
const
|
|
1773
|
-
|
|
2103
|
+
const head = segments[1];
|
|
2104
|
+
if (head?.startsWith(GJC_SESSION_PREFIX)) {
|
|
2105
|
+
const bound = planningSessionDirName(sessionId);
|
|
2106
|
+
if (!bound || head !== bound) return false;
|
|
2107
|
+
return segments[2] === "specs" || segments[2] === "plans";
|
|
2108
|
+
}
|
|
2109
|
+
return head === "specs" || head === "plans";
|
|
1774
2110
|
}
|
|
1775
2111
|
function isBlockedGjcPath(cwd: string, rawPath: string): boolean {
|
|
1776
2112
|
const segments = relativeGjcSegments(cwd, rawPath);
|
|
@@ -1781,21 +2117,119 @@ function hasBlockedGjcTarget(cwd: string, targets: ExtractedTargets): boolean {
|
|
|
1781
2117
|
return targets.paths.some(rawPath => isBlockedGjcPath(cwd, rawPath));
|
|
1782
2118
|
}
|
|
1783
2119
|
|
|
1784
|
-
function allTargetsAllowlisted(cwd: string, targets: ExtractedTargets): boolean {
|
|
2120
|
+
function allTargetsAllowlisted(cwd: string, targets: ExtractedTargets, sessionId?: string | null): boolean {
|
|
1785
2121
|
return (
|
|
1786
|
-
!targets.unknown &&
|
|
2122
|
+
!targets.unknown &&
|
|
2123
|
+
targets.paths.length > 0 &&
|
|
2124
|
+
targets.paths.every(rawPath => isAllowlistedPath(cwd, rawPath, sessionId))
|
|
1787
2125
|
);
|
|
1788
2126
|
}
|
|
1789
2127
|
|
|
1790
|
-
|
|
2128
|
+
/** POSIX directories that are temp roots even when `TMPDIR` is unset or poisoned. */
|
|
2129
|
+
const POSIX_SYSTEM_TEMP_ROOTS = ["/tmp", "/var/tmp", "/private/tmp", "/private/var/tmp"];
|
|
2130
|
+
|
|
2131
|
+
/**
|
|
2132
|
+
* `os.tmpdir()` follows `TMPDIR`, and Bun copies `cwd/.env` into the environment
|
|
2133
|
+
* when that variable was unset. A repository can therefore name an arbitrary
|
|
2134
|
+
* directory. Declarations below are not neutral scratch roots.
|
|
2135
|
+
*/
|
|
2136
|
+
function tempEnvNames(): readonly string[] {
|
|
2137
|
+
return process.platform === "win32" ? ["TEMP", "TMP", "TMPDIR"] : ["TMPDIR"];
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
const PROJECT_TEMP_DOTENV_MAX_FILE_BYTES = 64 * 1024;
|
|
2141
|
+
|
|
2142
|
+
// The snapshot is the cwd at module import, not a directory created later or a chdir before this module is imported.
|
|
2143
|
+
const startupTempSnapshot = projectEnvSnapshot(process.cwd(), {
|
|
2144
|
+
maxFileBytes: PROJECT_TEMP_DOTENV_MAX_FILE_BYTES,
|
|
2145
|
+
});
|
|
2146
|
+
|
|
2147
|
+
function projectTempSnapshots(cwd: string): ProjectEnvSnapshot[] {
|
|
2148
|
+
// Check both the cwd's .env and the startup snapshot (for module-import-time cwd).
|
|
2149
|
+
// The cwd's .env catches project-declared TMPDIR in the current working directory.
|
|
2150
|
+
// The startup snapshot is a fallback for when the cwd's .env is deleted or inaccessible
|
|
2151
|
+
// (e.g., after unlink or chdir), and also handles the case where cwd differs from the
|
|
2152
|
+
// module-import-time cwd. These workspace-controlled files are read as bounded regular files.
|
|
2153
|
+
const snapshots: ProjectEnvSnapshot[] = [
|
|
2154
|
+
projectEnvSnapshot(cwd, { maxFileBytes: PROJECT_TEMP_DOTENV_MAX_FILE_BYTES }),
|
|
2155
|
+
startupTempSnapshot,
|
|
2156
|
+
];
|
|
2157
|
+
return snapshots;
|
|
2158
|
+
}
|
|
2159
|
+
|
|
2160
|
+
/** macOS account temp directory. `getconf` does not read `TMPDIR`. */
|
|
2161
|
+
let darwinUserTempDirCache: string | undefined | null = null;
|
|
2162
|
+
function darwinUserTempDir(): string | undefined {
|
|
2163
|
+
if (process.platform !== "darwin") return undefined;
|
|
2164
|
+
if (darwinUserTempDirCache !== null) return darwinUserTempDirCache;
|
|
2165
|
+
darwinUserTempDirCache = undefined;
|
|
2166
|
+
try {
|
|
2167
|
+
const result = Bun.spawnSync({
|
|
2168
|
+
cmd: ["/usr/bin/getconf", "DARWIN_USER_TEMP_DIR"],
|
|
2169
|
+
env: { PATH: "/usr/bin:/bin:/usr/sbin:/sbin", LC_ALL: "C" },
|
|
2170
|
+
stdout: "pipe",
|
|
2171
|
+
stderr: "ignore",
|
|
2172
|
+
});
|
|
2173
|
+
if (result.exitCode !== 0) return undefined;
|
|
2174
|
+
const value = new TextDecoder().decode(result.stdout).trim();
|
|
2175
|
+
if (value) darwinUserTempDirCache = value;
|
|
2176
|
+
} catch {
|
|
2177
|
+
// Fixed `/tmp` roots still cover the POSIX default.
|
|
2178
|
+
}
|
|
2179
|
+
return darwinUserTempDirCache ?? undefined;
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
function addResolvedTempRoot(roots: Set<string>, value: string | undefined): void {
|
|
2183
|
+
const trimmed = value?.trim();
|
|
2184
|
+
if (trimmed) roots.add(path.resolve(trimmed));
|
|
2185
|
+
}
|
|
2186
|
+
|
|
2187
|
+
function isPlatformTempPath(resolved: string): boolean {
|
|
2188
|
+
for (const root of POSIX_SYSTEM_TEMP_ROOTS) {
|
|
2189
|
+
const resolvedRoot = path.resolve(root);
|
|
2190
|
+
if (resolved === resolvedRoot || isPathWithin(resolvedRoot, resolved)) return true;
|
|
2191
|
+
}
|
|
2192
|
+
const darwin = darwinUserTempDir();
|
|
2193
|
+
if (!darwin) return false;
|
|
2194
|
+
const resolvedDarwin = path.resolve(darwin);
|
|
2195
|
+
return resolved === resolvedDarwin || isPathWithin(resolvedDarwin, resolved);
|
|
2196
|
+
}
|
|
2197
|
+
|
|
2198
|
+
/** True when `live` is declared, or cannot be trusted because a dotenv layer was unsafe to read. */
|
|
2199
|
+
function projectDeclaresTempPath(snapshots: readonly ProjectEnvSnapshot[], live: string): boolean {
|
|
2200
|
+
const trimmedLive = live.trim();
|
|
2201
|
+
if (!trimmedLive) return false;
|
|
2202
|
+
const resolvedLive = path.resolve(trimmedLive);
|
|
2203
|
+
for (const snapshot of snapshots) {
|
|
2204
|
+
if (snapshot.incomplete && !isPlatformTempPath(resolvedLive)) return true;
|
|
2205
|
+
for (const name of tempEnvNames()) {
|
|
2206
|
+
const key = canonicalEnvKey(name);
|
|
2207
|
+
const declared = snapshot.values[key];
|
|
2208
|
+
if (declared === undefined) continue;
|
|
2209
|
+
if (snapshot.dynamic.has(key)) {
|
|
2210
|
+
// Expansion is not comparable. A dynamic value that landed on a
|
|
2211
|
+
// real system temp dir is already allowed through those roots.
|
|
2212
|
+
if (!isPlatformTempPath(resolvedLive)) return true;
|
|
2213
|
+
continue;
|
|
2214
|
+
}
|
|
2215
|
+
const trimmedDeclared = declared.trim();
|
|
2216
|
+
if (trimmedDeclared && path.resolve(trimmedDeclared) === resolvedLive) return true;
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
return false;
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2222
|
+
function neutralTempRoots(cwd: string): string[] {
|
|
1791
2223
|
const roots = new Set<string>();
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
2224
|
+
for (const fixed of POSIX_SYSTEM_TEMP_ROOTS) addResolvedTempRoot(roots, fixed);
|
|
2225
|
+
addResolvedTempRoot(roots, darwinUserTempDir());
|
|
2226
|
+
const candidates = [os.tmpdir(), process.env.TMPDIR];
|
|
2227
|
+
if (process.platform === "win32") candidates.push(process.env.TEMP, process.env.TMP);
|
|
2228
|
+
const snapshots = projectTempSnapshots(cwd);
|
|
2229
|
+
for (const candidate of candidates) {
|
|
2230
|
+
if (!candidate || projectDeclaresTempPath(snapshots, candidate)) continue;
|
|
2231
|
+
addResolvedTempRoot(roots, candidate);
|
|
2232
|
+
}
|
|
1799
2233
|
return [...roots];
|
|
1800
2234
|
}
|
|
1801
2235
|
|
|
@@ -1849,18 +2283,27 @@ async function isNeutralTempPath(cwd: string, rawPath: string): Promise<boolean>
|
|
|
1849
2283
|
if (unknown || !absolutePath) return false;
|
|
1850
2284
|
const resolvedCwd = path.resolve(cwd);
|
|
1851
2285
|
if (isPathWithin(resolvedCwd, absolutePath)) return false;
|
|
1852
|
-
|
|
2286
|
+
const roots = neutralTempRoots(cwd);
|
|
2287
|
+
const realRoots = await Promise.all(roots.map(realpathOrSelf));
|
|
2288
|
+
// Accept both spellings (`/var/folders` and `/private/var/folders`) before the
|
|
2289
|
+
// canonical re-check. The re-check still rejects a temp path whose real
|
|
2290
|
+
// target is inside the project.
|
|
2291
|
+
const lexicalRoots = new Set([...roots, ...realRoots]);
|
|
2292
|
+
if (![...lexicalRoots].some(root => isPathWithin(root, absolutePath))) return false;
|
|
1853
2293
|
const realTarget = await canonicalizeForContainment(absolutePath);
|
|
1854
2294
|
if (isPathWithin(await realpathOrSelf(resolvedCwd), realTarget)) return false;
|
|
1855
|
-
const realRoots = await Promise.all(neutralTempRoots().map(realpathOrSelf));
|
|
1856
2295
|
return realRoots.some(root => isPathWithin(root, realTarget));
|
|
1857
2296
|
}
|
|
1858
2297
|
|
|
1859
2298
|
/** Targets that remain disallowed during a planning phase (excludes neutral temp scratch). */
|
|
1860
|
-
async function planningBlockedTargets(
|
|
2299
|
+
async function planningBlockedTargets(
|
|
2300
|
+
cwd: string,
|
|
2301
|
+
targets: ExtractedTargets,
|
|
2302
|
+
sessionId?: string | null,
|
|
2303
|
+
): Promise<string[]> {
|
|
1861
2304
|
const blocked: string[] = [];
|
|
1862
2305
|
for (const rawPath of targets.paths) {
|
|
1863
|
-
if (isAllowlistedPath(cwd, rawPath)) continue;
|
|
2306
|
+
if (isAllowlistedPath(cwd, rawPath, sessionId)) continue;
|
|
1864
2307
|
if (!(await isNeutralTempPath(cwd, rawPath))) blocked.push(rawPath);
|
|
1865
2308
|
}
|
|
1866
2309
|
return blocked;
|
|
@@ -1882,12 +2325,15 @@ export async function assertWorkflowMutationRawPathsAllowed(input: {
|
|
|
1882
2325
|
throw new ToolError(`${WORKFLOW_STATE_MUTATION_BLOCK_MESSAGE}\nUse: ${command}`);
|
|
1883
2326
|
}
|
|
1884
2327
|
if (input.forceOverride) return;
|
|
1885
|
-
const
|
|
2328
|
+
const guardContext =
|
|
2329
|
+
input.guardContext ??
|
|
2330
|
+
(await readWorkflowGuardContext(input.cwd, { sessionId: input.sessionId, threadId: input.threadId }));
|
|
2331
|
+
const planning = await getActivePlanningSkill(input.cwd, input.sessionId, input.threadId, guardContext);
|
|
1886
2332
|
|
|
1887
2333
|
if (!planning) return;
|
|
1888
2334
|
const message = planningPhaseBlockMessage(planning.skill);
|
|
1889
2335
|
if (input.rawPaths.length === 0) throw new ToolError(message);
|
|
1890
|
-
const blocked = await planningBlockedTargets(input.cwd, targets);
|
|
2336
|
+
const blocked = await planningBlockedTargets(input.cwd, targets, guardContext.sessionId);
|
|
1891
2337
|
if (blocked.length > 0) throw new ToolError(message);
|
|
1892
2338
|
}
|
|
1893
2339
|
|
|
@@ -1907,7 +2353,10 @@ export async function getWorkflowMutationDecision(
|
|
|
1907
2353
|
command,
|
|
1908
2354
|
};
|
|
1909
2355
|
}
|
|
1910
|
-
const
|
|
2356
|
+
const guardContext =
|
|
2357
|
+
input.guardContext ??
|
|
2358
|
+
(await readWorkflowGuardContext(input.cwd, { sessionId: input.sessionId, threadId: input.threadId }));
|
|
2359
|
+
const planning = await getActivePlanningSkill(input.cwd, input.sessionId, input.threadId, guardContext);
|
|
1911
2360
|
if (!planning) {
|
|
1912
2361
|
return { blocked: false, targets: [] };
|
|
1913
2362
|
}
|
|
@@ -1919,12 +2368,20 @@ export async function getWorkflowMutationDecision(
|
|
|
1919
2368
|
// name is not authorization: an `autoresearch/*` branch isolates keep/discard
|
|
1920
2369
|
// bookkeeping, it does not turn product edits into research.
|
|
1921
2370
|
//
|
|
2371
|
+
// Shell and code-execution tools can run multiple operations before writing,
|
|
2372
|
+
// so checking the harness once cannot protect the later write destination.
|
|
2373
|
+
// Only direct file-mutation tools may use the harness exception.
|
|
1922
2374
|
// The allowance requires at least one named target: `every` is vacuously true
|
|
1923
2375
|
// for an empty list, so an opaque mutation that resolves to NO target (e.g.
|
|
1924
2376
|
// `python3 -c "open('src/product.ts','w').write('x')"`) would otherwise skip
|
|
1925
2377
|
// the fail-closed check below and be waved through on a research mission.
|
|
2378
|
+
// Bash is a compound command list, not a write boundary: an earlier `ln` or
|
|
2379
|
+
// `mv` can retarget the harness before a later redirection. Harness changes
|
|
2380
|
+
// must use a single-target file tool so the final path is checked for symlinks
|
|
2381
|
+
// immediately before that write operation.
|
|
1926
2382
|
if (
|
|
1927
2383
|
planning.skill === "autoresearch" &&
|
|
2384
|
+
AUTORESEARCH_HARNESS_FILE_TOOL_NAMES.has(input.tool.name) &&
|
|
1928
2385
|
targets.paths.length > 0 &&
|
|
1929
2386
|
!targets.unknown &&
|
|
1930
2387
|
targets.paths.every(p => isAutoresearchAuthorizedResearchPath(input.cwd, p))
|
|
@@ -1946,7 +2403,7 @@ export async function getWorkflowMutationDecision(
|
|
|
1946
2403
|
// Neutral temp scratch (outside the project tree) stays writable so agents can
|
|
1947
2404
|
// stage artifacts and feed their path to the sanctioned `gjc ... --write` CLIs.
|
|
1948
2405
|
// Read-only / `gjc` bash extract no targets and fall through to allowed here.
|
|
1949
|
-
const blockedTargets = await planningBlockedTargets(input.cwd, targets);
|
|
2406
|
+
const blockedTargets = await planningBlockedTargets(input.cwd, targets, guardContext.sessionId);
|
|
1950
2407
|
if (blockedTargets.length === 0) {
|
|
1951
2408
|
return { blocked: false, targets: targets.paths };
|
|
1952
2409
|
}
|
|
@@ -1954,7 +2411,9 @@ export async function getWorkflowMutationDecision(
|
|
|
1954
2411
|
blocked: true,
|
|
1955
2412
|
message,
|
|
1956
2413
|
targets: targets.paths,
|
|
1957
|
-
reason: allTargetsAllowlisted(input.cwd, targets)
|
|
2414
|
+
reason: allTargetsAllowlisted(input.cwd, targets, guardContext.sessionId)
|
|
2415
|
+
? "handoff-artifact-tool-target"
|
|
2416
|
+
: "phase-boundary",
|
|
1958
2417
|
};
|
|
1959
2418
|
}
|
|
1960
2419
|
|