@gajae-code/coding-agent 0.18.12 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +467 -0
- package/dist/types/capability/rule.d.ts +7 -2
- package/dist/types/cli/append-system-prompt.d.ts +6 -0
- package/dist/types/cli/args.d.ts +7 -2
- package/dist/types/cli/customize-doctor-precedence.d.ts +16 -0
- package/dist/types/cli/customize-doctor.d.ts +4 -3
- package/dist/types/cli/public-command-errors.d.ts +1 -0
- package/dist/types/cli/root-flags.d.ts +0 -12
- package/dist/types/cli/update-cli.d.ts +7 -2
- package/dist/types/cli-main.d.ts +0 -6
- package/dist/types/commands/launch.d.ts +0 -6
- package/dist/types/commands/migrate.d.ts +1 -1
- package/dist/types/commands/sdk.d.ts +8 -0
- package/dist/types/commit/map-reduce/index.d.ts +4 -4
- package/dist/types/config/autorouting-tier-map.d.ts +23 -1
- package/dist/types/config/config-hot-reload.d.ts +5 -0
- package/dist/types/config/file-lock.d.ts +44 -2
- package/dist/types/config/model-profile-activation.d.ts +19 -5
- package/dist/types/config/model-registry.d.ts +16 -3
- package/dist/types/config/model-resolver.d.ts +5 -4
- package/dist/types/config/settings-schema.d.ts +11 -1
- package/dist/types/coordinator-mcp/server.d.ts +4 -0
- package/dist/types/customization/import.d.ts +2 -0
- package/dist/types/customization/types.d.ts +1 -1
- package/dist/types/dap/session.d.ts +11 -2
- package/dist/types/discovery/helpers.d.ts +7 -2
- package/dist/types/edit/index.d.ts +5 -0
- package/dist/types/edit/modes/patch.d.ts +9 -0
- package/dist/types/eval/py/runtime.d.ts +9 -0
- package/dist/types/export/ttsr.d.ts +19 -0
- package/dist/types/extensibility/extensions/types.d.ts +5 -1
- package/dist/types/extensibility/slash-commands.d.ts +1 -0
- package/dist/types/gjc-runtime/ralplan-current-final.d.ts +54 -0
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +5 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +24 -0
- package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +6 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +1 -0
- package/dist/types/harness-control-plane/receipt-spool.d.ts +4 -0
- package/dist/types/hindsight/content.d.ts +5 -6
- package/dist/types/internal-urls/types.d.ts +2 -0
- package/dist/types/lsp/clients/swiftlint-client.d.ts +0 -4
- package/dist/types/main.d.ts +23 -18
- package/dist/types/migrate/action-planner.d.ts +10 -1
- package/dist/types/migrate/adapters/codex.d.ts +1 -1
- package/dist/types/migrate/adapters/index.d.ts +35 -3
- package/dist/types/migrate/hook-module.d.ts +23 -0
- package/dist/types/migrate/report.d.ts +2 -1
- package/dist/types/migrate/types.d.ts +69 -2
- package/dist/types/modes/components/agent-dashboard.d.ts +1 -1
- package/dist/types/modes/components/model-selector.d.ts +43 -0
- package/dist/types/modes/prompt-suggestion-controller.d.ts +1 -1
- package/dist/types/modes/utils/host-status.d.ts +15 -11
- package/dist/types/runtime-mcp/project-host-values.d.ts +11 -5
- package/dist/types/sdk/broker/broker-exit.d.ts +1 -1
- package/dist/types/sdk/broker/broker.d.ts +22 -1
- package/dist/types/sdk/broker/ensure.d.ts +5 -2
- package/dist/types/sdk/broker/hop.d.ts +9 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +22 -0
- package/dist/types/sdk/broker/session-index.d.ts +2 -0
- package/dist/types/sdk/broker/transport.d.ts +2 -0
- package/dist/types/sdk/bus/index.d.ts +21 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +7 -1
- package/dist/types/sdk/bus/telegram-daemon.d.ts +2 -0
- package/dist/types/sdk/bus/threaded-render.d.ts +2 -1
- package/dist/types/sdk/host/control/operations.d.ts +5 -1
- package/dist/types/sdk/host/events.d.ts +4 -0
- package/dist/types/sdk/host/host.d.ts +2 -1
- package/dist/types/sdk/host/prompt-image-upload.d.ts +43 -0
- package/dist/types/sdk/host/session-runtime.d.ts +8 -8
- package/dist/types/sdk/host/surface-policy.d.ts +2 -1
- package/dist/types/sdk/lifecycle/service.d.ts +12 -0
- package/dist/types/sdk/prompt-status.d.ts +11 -4
- package/dist/types/session/agent-session.d.ts +50 -8
- package/dist/types/session/session-manager.d.ts +16 -0
- package/dist/types/skill-state/workflow-mutation-guard.d.ts +13 -1
- package/dist/types/slash-commands/helpers/mcp.d.ts +5 -0
- package/dist/types/ssh/ssh-executor.d.ts +8 -1
- package/dist/types/system-prompt.d.ts +3 -2
- package/dist/types/task/role-body.d.ts +11 -0
- package/dist/types/tools/awaiting-input.d.ts +28 -0
- package/dist/types/tools/bash-allowed-prefixes.d.ts +6 -0
- package/dist/types/tools/bash-skill-urls.d.ts +4 -0
- package/dist/types/tools/bash.d.ts +7 -0
- package/dist/types/tools/descriptors.d.ts +13 -2
- package/dist/types/tools/fetch.d.ts +6 -0
- package/dist/types/tools/irc.d.ts +5 -3
- package/dist/types/tools/tmux-self-injection-guard.d.ts +2 -0
- package/dist/types/tools/ultragoal-ask-guard.d.ts +1 -1
- package/dist/types/tools/write.d.ts +2 -0
- package/dist/types/utils/file-mentions.d.ts +5 -0
- package/dist/types/utils/image-limits.d.ts +7 -0
- package/dist/types/utils/image-loading.d.ts +0 -1
- package/dist/types/utils/open.d.ts +0 -1
- package/dist/types/utils/pasted-image-loading.d.ts +1 -5
- package/dist/types/utils/prompt-suggestion.d.ts +2 -2
- package/dist/types/utils/shell-snapshot.d.ts +2 -0
- package/dist/types/utils/title-generator.d.ts +13 -3
- package/dist/types/web/search/providers/codex.d.ts +6 -7
- package/package.json +9 -8
- package/scripts/acp-conformance-agent.ts +12 -2
- package/scripts/generate-sdk-operation-inventory.ts +16 -0
- package/src/capability/fs.ts +31 -3
- package/src/capability/rule.ts +11 -2
- package/src/cli/append-system-prompt.ts +12 -0
- package/src/cli/args.ts +11 -7
- package/src/cli/customize-doctor-precedence.ts +31 -0
- package/src/cli/customize-doctor.ts +49 -39
- package/src/cli/fast-help.ts +0 -2
- package/src/cli/migrate-cli.ts +14 -3
- package/src/cli/public-command-errors.ts +2 -0
- package/src/cli/root-flags.ts +0 -2
- package/src/cli/update-cli.ts +247 -6
- package/src/commands/harness.ts +25 -15
- package/src/commands/migrate.ts +3 -3
- package/src/commands/sdk.ts +81 -11
- package/src/commit/agentic/agent.ts +6 -6
- package/src/commit/analysis/conventional.ts +14 -5
- package/src/commit/map-reduce/index.ts +7 -7
- package/src/commit/pipeline.ts +12 -12
- package/src/config/autorouting-tier-map.ts +20 -1
- package/src/config/config-file.ts +106 -3
- package/src/config/config-hot-reload.ts +8 -0
- package/src/config/file-lock.ts +1431 -304
- package/src/config/keybindings.ts +154 -7
- package/src/config/model-profile-activation.ts +177 -39
- package/src/config/model-registry.ts +380 -55
- package/src/config/model-resolver.ts +49 -21
- package/src/config/settings-schema.ts +13 -1
- package/src/config/settings.ts +2 -0
- package/src/coordinator-mcp/server.ts +37 -2
- package/src/crash/sanitize.ts +3 -1
- package/src/customization/import.ts +276 -7
- package/src/customization/inventory.ts +24 -8
- package/src/customization/types.ts +1 -0
- package/src/dap/client.ts +3 -2
- package/src/dap/session.ts +161 -36
- package/src/debug/crash-diagnostics.ts +87 -5
- package/src/debug/report-bundle.ts +71 -2
- package/src/discovery/builtin.ts +4 -3
- package/src/discovery/helpers.ts +24 -14
- package/src/edit/index.ts +191 -5
- package/src/edit/modes/patch.ts +27 -0
- package/src/eval/py/runtime.ts +44 -4
- package/src/exec/non-interactive-env.ts +0 -1
- package/src/export/html/index.ts +60 -20
- package/src/export/ttsr.ts +176 -1
- package/src/extensibility/extensions/loader.ts +2 -0
- package/src/extensibility/extensions/ouroboros-ooo-bridge.ts +63 -1
- package/src/extensibility/extensions/types.ts +5 -1
- package/src/extensibility/hooks/loader.ts +10 -2
- package/src/extensibility/plugins/marketplace/manager.ts +36 -10
- package/src/extensibility/slash-commands.ts +5 -0
- package/src/gjc-runtime/gc-runtime.ts +4 -1
- package/src/gjc-runtime/launch-tmux.ts +23 -13
- package/src/gjc-runtime/ralplan-current-final.ts +294 -0
- package/src/gjc-runtime/ralplan-runtime.ts +95 -1
- package/src/gjc-runtime/state-writer.ts +109 -5
- package/src/gjc-runtime/tmux-sessions.ts +93 -13
- package/src/gjc-runtime/ultragoal-change-set.ts +90 -11
- package/src/gjc-runtime/ultragoal-guard.ts +9 -1
- package/src/gjc-runtime/ultragoal-runtime.ts +15 -5
- package/src/harness-control-plane/finalize.ts +11 -0
- package/src/harness-control-plane/receipt-spool.ts +105 -28
- package/src/harness-control-plane/storage.ts +124 -6
- package/src/hindsight/content.ts +116 -16
- package/src/hindsight/mental-models.ts +22 -10
- package/src/hindsight/state.ts +7 -4
- package/src/hooks/native-skill-hook.ts +3 -1
- package/src/internal-urls/artifact-protocol.ts +3 -1
- package/src/internal-urls/docs-index.generated.ts +14 -14
- package/src/internal-urls/local-protocol.ts +28 -3
- package/src/internal-urls/types.ts +2 -0
- package/src/lsp/client.ts +2 -1
- package/src/lsp/clients/biome-client.ts +2 -0
- package/src/lsp/clients/swiftlint-client.ts +2 -0
- package/src/main.ts +330 -92
- package/src/memories/index.ts +25 -2
- package/src/migrate/action-planner.ts +517 -2
- package/src/migrate/adapters/claude-code.ts +141 -3
- package/src/migrate/adapters/codex.ts +57 -5
- package/src/migrate/adapters/index.ts +240 -5
- package/src/migrate/adapters/opencode.ts +4 -0
- package/src/migrate/executor.ts +141 -3
- package/src/migrate/hook-module.ts +193 -0
- package/src/migrate/report.ts +45 -1
- package/src/migrate/types.ts +73 -3
- package/src/modes/acp/acp-agent.ts +10 -1
- package/src/modes/components/agent-dashboard.ts +20 -4
- package/src/modes/components/customization/customization-dashboard.ts +7 -0
- package/src/modes/components/customization/import-wizard.ts +29 -3
- package/src/modes/components/model-selector.ts +157 -103
- package/src/modes/components/welcome.ts +3 -1
- package/src/modes/controllers/command-controller.ts +271 -16
- package/src/modes/controllers/event-controller.ts +1 -1
- package/src/modes/controllers/extension-ui-controller.ts +31 -1
- package/src/modes/controllers/goal-mode-controller.ts +79 -10
- package/src/modes/controllers/input-controller.ts +2 -0
- package/src/modes/controllers/plan-mode-controller.ts +49 -4
- package/src/modes/controllers/runtime-mcp-command-controller.ts +7 -6
- package/src/modes/controllers/selector-controller.ts +14 -1
- package/src/modes/interactive-mode.ts +55 -4
- package/src/modes/prompt-suggestion-controller.ts +1 -1
- package/src/modes/runtime-init.ts +5 -1
- package/src/modes/utils/host-status.ts +21 -10
- package/src/prompts/agent-fragments/browser-aside.md +2 -2
- package/src/prompts/system/irc-incoming.md +3 -1
- package/src/prompts/tools/irc.md +1 -1
- package/src/runtime-mcp/manager.ts +8 -3
- package/src/runtime-mcp/project-host-values.ts +12 -8
- package/src/runtime-mcp/tool-bridge.ts +68 -4
- package/src/runtime-mcp/transports/stdio.ts +2 -1
- package/src/sdk/broker/broker-exit.ts +2 -0
- package/src/sdk/broker/broker.ts +143 -4
- package/src/sdk/broker/ensure.ts +74 -6
- package/src/sdk/broker/hop.ts +13 -1
- package/src/sdk/broker/lifecycle-ledger.ts +3 -0
- package/src/sdk/broker/lifecycle.ts +102 -6
- package/src/sdk/broker/session-index.ts +29 -4
- package/src/sdk/broker/transport.ts +17 -1
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/index.ts +408 -14
- package/src/sdk/bus/session-control.ts +2 -0
- package/src/sdk/bus/slack-daemon.ts +105 -9
- package/src/sdk/bus/telegram-daemon-contract.ts +7 -1
- package/src/sdk/bus/telegram-daemon.ts +10 -2
- package/src/sdk/bus/threaded-render.ts +20 -1
- package/src/sdk/cli/session-cli.ts +2 -0
- package/src/sdk/host/control/dispatch.ts +13 -1
- package/src/sdk/host/control/operations.ts +10 -1
- package/src/sdk/host/events.ts +67 -4
- package/src/sdk/host/host.ts +8 -2
- package/src/sdk/host/prompt-image-upload.ts +438 -0
- package/src/sdk/host/sdk-surface-parity.test.ts +15 -0
- package/src/sdk/host/session-runtime.test.ts +1537 -24
- package/src/sdk/host/session-runtime.ts +332 -36
- package/src/sdk/host/surface-policy.ts +4 -0
- package/src/sdk/host/websocket-transport.lifecycle.test.ts +90 -2
- package/src/sdk/host/websocket-transport.ts +19 -7
- package/src/sdk/lifecycle/service.ts +14 -1
- package/src/sdk/prompt-status.ts +6 -4
- package/src/sdk/protocol/adapter-validation.ts +28 -1
- package/src/sdk/protocol/operation-inventory.generated.json +160 -0
- package/src/sdk/protocol/operation-registry.ts +33 -3
- package/src/sdk/session.ts +123 -36
- package/src/session/agent-session.ts +1614 -329
- package/src/session/history-storage.ts +182 -12
- package/src/session/internal/managed-session-scope.ts +47 -18
- package/src/session/internal/managed-session-storage.ts +71 -1
- package/src/session/session-manager.ts +90 -5
- package/src/session-import/redact.ts +53 -2
- package/src/skill-state/workflow-mutation-guard.ts +1498 -18
- package/src/slash-commands/builtin-registry.ts +6 -3
- package/src/slash-commands/helpers/mcp.ts +11 -3
- package/src/ssh/ssh-executor.ts +9 -2
- package/src/stt/recorder.ts +26 -9
- package/src/system-prompt.ts +61 -16
- package/src/task/executor.ts +12 -8
- package/src/task/role-body.ts +84 -0
- package/src/task/token-log-mode.test.ts +162 -0
- package/src/task/token-log.ts +91 -2
- package/src/tools/archive-reader.ts +650 -37
- package/src/tools/ask.ts +1 -0
- package/src/tools/ast-grep.ts +7 -2
- package/src/tools/auto-generated-guard.ts +12 -3
- package/src/tools/awaiting-input.ts +114 -0
- package/src/tools/bash-allowed-prefixes.ts +61 -1
- package/src/tools/bash-skill-urls.ts +58 -5
- package/src/tools/bash.ts +99 -26
- package/src/tools/browser/tab-worker.ts +39 -13
- package/src/tools/browser.ts +17 -7
- package/src/tools/cron.ts +10 -1
- package/src/tools/debug.ts +9 -2
- package/src/tools/descriptors.test.ts +4 -4
- package/src/tools/descriptors.ts +16 -4
- package/src/tools/eval.ts +15 -0
- package/src/tools/fetch.ts +90 -2
- package/src/tools/gh.ts +27 -12
- package/src/tools/image-gen.ts +20 -6
- package/src/tools/irc.ts +5 -3
- package/src/tools/monitor.ts +9 -1
- package/src/tools/output-meta.ts +51 -3
- package/src/tools/read.ts +9 -2
- package/src/tools/recipe/runner.ts +23 -12
- package/src/tools/render-utils.ts +2 -2
- package/src/tools/search.ts +3 -2
- package/src/tools/ssh.ts +274 -8
- package/src/tools/subagent.ts +10 -4
- package/src/tools/tmux-self-injection-guard.ts +3301 -122
- package/src/tools/tool-catalog.generated.ts +1 -1
- package/src/tools/ultragoal-ask-guard.ts +13 -1
- package/src/tools/write.ts +7 -0
- package/src/tui/tree-list.ts +15 -1
- package/src/utils/commit-message-generator.ts +3 -2
- package/src/utils/external-editor.ts +21 -7
- package/src/utils/file-mentions.ts +46 -6
- package/src/utils/git.ts +38 -7
- package/src/utils/image-limits.ts +8 -0
- package/src/utils/image-loading.ts +1 -1
- package/src/utils/open.ts +73 -4
- package/src/utils/pasted-image-loading.ts +9 -8
- package/src/utils/prompt-suggestion.ts +5 -4
- package/src/utils/shell-snapshot.ts +9 -1
- package/src/utils/title-generator.ts +65 -20
- package/src/web/search/index.ts +2 -1
- package/src/web/search/providers/codex.ts +29 -32
|
@@ -57,7 +57,7 @@ import {
|
|
|
57
57
|
invalidateSessionTitleGeneration,
|
|
58
58
|
isSessionTitleGenerationCurrent,
|
|
59
59
|
} from "../utils/session-title-generation";
|
|
60
|
-
import { buildConversationTitleInput, generateSessionTitle } from "../utils/title-generator";
|
|
60
|
+
import { buildConversationTitleInput, generateSessionTitle, titleInputObfuscator } from "../utils/title-generator";
|
|
61
61
|
import { handleAsideAcp } from "./helpers/aside";
|
|
62
62
|
import { buildAutoroutingStatusReport } from "./helpers/autorouting-status";
|
|
63
63
|
import { buildContextReportText } from "./helpers/context-report";
|
|
@@ -158,7 +158,9 @@ function toSlashCommandRuntime(runtime: TuiSlashCommandRuntime): SlashCommandRun
|
|
|
158
158
|
async function regenerateSessionTitle(runtime: SlashCommandRuntime): Promise<SlashCommandResult> {
|
|
159
159
|
try {
|
|
160
160
|
const generation = beginSessionTitleGeneration(runtime.sessionManager);
|
|
161
|
-
const
|
|
161
|
+
const activeCwd = runtime.sessionManager.getCwd();
|
|
162
|
+
const obfuscateText = await titleInputObfuscator(runtime.settings, activeCwd);
|
|
163
|
+
const input = buildConversationTitleInput(runtime.session.messages, obfuscateText);
|
|
162
164
|
if (!input) {
|
|
163
165
|
return usage("Nothing to summarize yet — pass a title: /rename <title>", runtime);
|
|
164
166
|
}
|
|
@@ -170,10 +172,11 @@ async function regenerateSessionTitle(runtime: SlashCommandRuntime): Promise<Sla
|
|
|
170
172
|
runtime.session.credentialSessionId,
|
|
171
173
|
runtime.session.model,
|
|
172
174
|
provider => runtime.session.agent.metadataForProvider(provider),
|
|
175
|
+
activeCwd,
|
|
173
176
|
);
|
|
174
177
|
if (
|
|
175
178
|
!isSessionTitleGenerationCurrent(runtime.sessionManager, generation) ||
|
|
176
|
-
buildConversationTitleInput(runtime.session.messages) !== input
|
|
179
|
+
buildConversationTitleInput(runtime.session.messages, obfuscateText) !== input
|
|
177
180
|
)
|
|
178
181
|
return commandConsumed();
|
|
179
182
|
if (!generated) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getMCPConfigPath } from "@gajae-code/utils";
|
|
1
|
+
import { getMCPConfigPath, sanitizeDisplayLine } from "@gajae-code/utils";
|
|
2
2
|
import { listPrompts, listResources, listTools } from "../../runtime-mcp/client";
|
|
3
3
|
import {
|
|
4
4
|
addMCPServer,
|
|
@@ -19,6 +19,14 @@ import { commandConsumed, errorMessage, parseNamedScopeArgs, parseSubcommand, us
|
|
|
19
19
|
|
|
20
20
|
type AcpMcpScope = "user" | "project";
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* One terminal row for an untrusted MCP server or tool name.
|
|
24
|
+
* Ordinary names are unchanged. Controls, OSC, and line breaks are not.
|
|
25
|
+
*/
|
|
26
|
+
export function mcpTerminalLabel(value: string): string {
|
|
27
|
+
return sanitizeDisplayLine(value);
|
|
28
|
+
}
|
|
29
|
+
|
|
22
30
|
interface ParsedMcpAddArgs {
|
|
23
31
|
name?: string;
|
|
24
32
|
scope: AcpMcpScope;
|
|
@@ -272,7 +280,7 @@ async function handleTestCommand(rest: string, runtime: SlashCommandRuntime): Pr
|
|
|
272
280
|
return await withPreparedMcpConnection(runtime, name, server.config, server.scope, async connection => {
|
|
273
281
|
const tools = await listTools(connection);
|
|
274
282
|
const lines = [`Server "${name}" connected (${tools.length} tools).`];
|
|
275
|
-
for (const tool of tools) lines.push(` - ${tool.name}`);
|
|
283
|
+
for (const tool of tools) lines.push(` - ${mcpTerminalLabel(tool.name)}`);
|
|
276
284
|
await runtime.output(lines.join("\n"));
|
|
277
285
|
return commandConsumed();
|
|
278
286
|
});
|
|
@@ -398,7 +406,7 @@ async function handleListCommand(runtime: SlashCommandRuntime): Promise<SlashCom
|
|
|
398
406
|
} else {
|
|
399
407
|
location = (config as { command: string }).command;
|
|
400
408
|
}
|
|
401
|
-
return `${name} | ${type} | ${status} | ${location ?? "(unknown)"} [${scope}]`;
|
|
409
|
+
return `${mcpTerminalLabel(name)} | ${type} | ${status} | ${location ?? "(unknown)"} [${scope}]`;
|
|
402
410
|
})
|
|
403
411
|
.join("\n"),
|
|
404
412
|
);
|
package/src/ssh/ssh-executor.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { logger, ptree } from "@gajae-code/utils";
|
|
2
2
|
import { Settings } from "../config/settings";
|
|
3
|
-
import { OutputSink } from "../session/streaming-output";
|
|
3
|
+
import { OutputSink, type TerminalArtifactPublisher } from "../session/streaming-output";
|
|
4
4
|
import { resolveOutputMaxColumns, resolveOutputSinkHeadBytes } from "../tools/output-meta";
|
|
5
5
|
import { buildRemoteCommand, ensureConnection, ensureHostInfo, type SSHConnectionTarget } from "./connection-manager";
|
|
6
6
|
import { hasSshfs, mountRemote } from "./sshfs-mount";
|
|
@@ -21,6 +21,8 @@ export interface SSHExecutorOptions {
|
|
|
21
21
|
/** Artifact path/id for full output storage */
|
|
22
22
|
artifactPath?: string;
|
|
23
23
|
artifactId?: string;
|
|
24
|
+
/** Session-owned publisher for managed sessions without a writable artifact path. */
|
|
25
|
+
artifactPublisher?: TerminalArtifactPublisher;
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
export interface SSHResult {
|
|
@@ -40,8 +42,12 @@ export interface SSHResult {
|
|
|
40
42
|
outputLines: number;
|
|
41
43
|
/** Number of bytes included in the output text */
|
|
42
44
|
outputBytes: number;
|
|
43
|
-
/** Artifact ID if
|
|
45
|
+
/** Artifact ID if output was saved to artifact storage. */
|
|
44
46
|
artifactId?: string;
|
|
47
|
+
/** Bytes omitted from artifact storage after its hard cap was reached. */
|
|
48
|
+
artifactTruncatedBytes?: number;
|
|
49
|
+
/** Bounded diagnostic when artifact storage failed. */
|
|
50
|
+
artifactFailureDiagnostic?: string;
|
|
45
51
|
}
|
|
46
52
|
|
|
47
53
|
function quoteForCompatShell(command: string): string {
|
|
@@ -92,6 +98,7 @@ export async function executeSSH(
|
|
|
92
98
|
onChunk: options?.onChunk,
|
|
93
99
|
artifactPath: options?.artifactPath,
|
|
94
100
|
artifactId: options?.artifactId,
|
|
101
|
+
artifactPublisher: options?.artifactPublisher,
|
|
95
102
|
headBytes: resolveOutputSinkHeadBytes(settings),
|
|
96
103
|
maxColumns: resolveOutputMaxColumns(settings),
|
|
97
104
|
});
|
package/src/stt/recorder.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
2
|
import * as os from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
-
import { $which, logger
|
|
4
|
+
import { $which, logger } from "@gajae-code/utils";
|
|
5
5
|
import { $ } from "bun";
|
|
6
6
|
|
|
7
7
|
export interface RecordingHandle {
|
|
@@ -212,10 +212,29 @@ if (Test-Path $outPath) {
|
|
|
212
212
|
}
|
|
213
213
|
`;
|
|
214
214
|
|
|
215
|
+
async function writePowerShellRecordingScript(): Promise<{ tempDir: string; scriptPath: string }> {
|
|
216
|
+
// Random directory plus an exclusive create: a preexisting symlink under the
|
|
217
|
+
// shared temp directory is not opened, and the script is not left world-readable.
|
|
218
|
+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "gjc-stt-record-"));
|
|
219
|
+
try {
|
|
220
|
+
if (process.platform !== "win32") await fs.chmod(tempDir, 0o700);
|
|
221
|
+
const scriptPath = path.join(tempDir, "record.ps1");
|
|
222
|
+
const handle = await fs.open(scriptPath, "wx", 0o600);
|
|
223
|
+
try {
|
|
224
|
+
await handle.writeFile(PS_RECORD_SCRIPT);
|
|
225
|
+
} finally {
|
|
226
|
+
await handle.close();
|
|
227
|
+
}
|
|
228
|
+
if (process.platform !== "win32") await fs.chmod(scriptPath, 0o600);
|
|
229
|
+
return { tempDir, scriptPath };
|
|
230
|
+
} catch (error) {
|
|
231
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
232
|
+
throw error;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
215
236
|
async function startPowerShellRecording(outputPath: string): Promise<RecordingHandle> {
|
|
216
|
-
|
|
217
|
-
const scriptPath = path.join(os.tmpdir(), `gjc-stt-record-${Snowflake.next()}.ps1`);
|
|
218
|
-
await Bun.write(scriptPath, PS_RECORD_SCRIPT);
|
|
237
|
+
const { tempDir, scriptPath } = await writePowerShellRecordingScript();
|
|
219
238
|
|
|
220
239
|
const proc = Bun.spawn(["powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", scriptPath, outputPath], {
|
|
221
240
|
stdin: "pipe",
|
|
@@ -224,7 +243,7 @@ async function startPowerShellRecording(outputPath: string): Promise<RecordingHa
|
|
|
224
243
|
});
|
|
225
244
|
|
|
226
245
|
proc.exited.then(() => {
|
|
227
|
-
fs.
|
|
246
|
+
fs.rm(tempDir, { recursive: true, force: true }).catch(() => {});
|
|
228
247
|
});
|
|
229
248
|
|
|
230
249
|
// Wait for "RECORDING" on stdout to confirm it started
|
|
@@ -250,8 +269,7 @@ async function startPowerShellRecording(outputPath: string): Promise<RecordingHa
|
|
|
250
269
|
if (proc.stderr && typeof proc.stderr !== "number") {
|
|
251
270
|
stderrText = await new Response(proc.stderr as ReadableStream).text();
|
|
252
271
|
}
|
|
253
|
-
|
|
254
|
-
fs.unlink(scriptPath).catch(() => {});
|
|
272
|
+
fs.rm(tempDir, { recursive: true, force: true }).catch(() => {});
|
|
255
273
|
throw new Error(
|
|
256
274
|
`PowerShell audio recording failed to start: ${stderrText.trim() || output.trim() || "(no output)"}`,
|
|
257
275
|
);
|
|
@@ -269,8 +287,7 @@ async function startPowerShellRecording(outputPath: string): Promise<RecordingHa
|
|
|
269
287
|
const killTimer = setTimeout(() => proc.kill(), 8000);
|
|
270
288
|
await proc.exited;
|
|
271
289
|
clearTimeout(killTimer);
|
|
272
|
-
|
|
273
|
-
fs.unlink(scriptPath).catch(() => {});
|
|
290
|
+
fs.rm(tempDir, { recursive: true, force: true }).catch(() => {});
|
|
274
291
|
},
|
|
275
292
|
};
|
|
276
293
|
}
|
package/src/system-prompt.ts
CHANGED
|
@@ -75,6 +75,31 @@ function sanitizeEmbeddedPromptContent(content: string): string {
|
|
|
75
75
|
return escapePromptMetadata(content, { preserveNewlines: true });
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
const UNTRUSTED_CONTENT_OPEN_LINE = "<untrusted-content>\n";
|
|
79
|
+
const UNTRUSTED_CONTENT_CLOSE_LINE = "\n</untrusted-content>";
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Encode an inventory description. MCP descriptions already include a host-owned
|
|
83
|
+
* untrusted-content fence; keep those delimiters and encode only the body.
|
|
84
|
+
* A body that still contains a closing fence is not that wrapper, so encode the whole value.
|
|
85
|
+
*/
|
|
86
|
+
function escapeInventoryDescription(description: string): string {
|
|
87
|
+
const openLength = UNTRUSTED_CONTENT_OPEN_LINE.length;
|
|
88
|
+
const closeLength = UNTRUSTED_CONTENT_CLOSE_LINE.length;
|
|
89
|
+
if (
|
|
90
|
+
description.startsWith(UNTRUSTED_CONTENT_OPEN_LINE) &&
|
|
91
|
+
description.endsWith(UNTRUSTED_CONTENT_CLOSE_LINE) &&
|
|
92
|
+
description.length >= openLength + closeLength
|
|
93
|
+
) {
|
|
94
|
+
const body = description.slice(openLength, -closeLength);
|
|
95
|
+
if (!/<\/untrusted-content>/i.test(body)) {
|
|
96
|
+
const encodedBody = escapePromptMetadata(body, { preserveNewlines: true });
|
|
97
|
+
return `${UNTRUSTED_CONTENT_OPEN_LINE}${encodedBody}${UNTRUSTED_CONTENT_CLOSE_LINE}`;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return escapePromptMetadata(description, { preserveNewlines: true });
|
|
101
|
+
}
|
|
102
|
+
|
|
78
103
|
function firstNonEmpty(...values: (string | undefined | null)[]): string | null {
|
|
79
104
|
for (const value of values) {
|
|
80
105
|
const trimmed = value?.trim();
|
|
@@ -345,8 +370,9 @@ export async function loadProjectContextFiles(
|
|
|
345
370
|
}
|
|
346
371
|
|
|
347
372
|
/**
|
|
348
|
-
* Load the
|
|
349
|
-
*
|
|
373
|
+
* Load the user system prompt customization from SYSTEM.md.
|
|
374
|
+
* A repository SYSTEM.md is untrusted project text. It must not replace the
|
|
375
|
+
* user file or become the customization block.
|
|
350
376
|
*/
|
|
351
377
|
export async function loadSystemPromptFiles(options: LoadContextFilesOptions = {}): Promise<string | null> {
|
|
352
378
|
const resolvedCwd = options.cwd ?? getProjectDir();
|
|
@@ -359,11 +385,6 @@ export async function loadSystemPromptFiles(options: LoadContextFilesOptions = {
|
|
|
359
385
|
|
|
360
386
|
if (result.items.length === 0) return null;
|
|
361
387
|
|
|
362
|
-
const projectLevel = result.items.find(item => item.level === "project");
|
|
363
|
-
if (projectLevel) {
|
|
364
|
-
return projectLevel.content;
|
|
365
|
-
}
|
|
366
|
-
|
|
367
388
|
const userLevel = result.items.find(item => item.level === "user");
|
|
368
389
|
return userLevel?.content ?? null;
|
|
369
390
|
}
|
|
@@ -745,18 +766,31 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
745
766
|
const toolPromptNames = new Map<string, string>(toolNames.map(name => [name, tools?.get(name)?.wireName ?? name]));
|
|
746
767
|
const toolRefs = Object.fromEntries(toolPromptNames.entries());
|
|
747
768
|
const hasHiddenToolDiscoveryTool = Object.hasOwn(toolRefs, "search_tool_bm25");
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
769
|
+
// The inventory template is compiled with noEscape. Encode tool metadata here so a
|
|
770
|
+
// tools/list label or description cannot close <inventory> or insert a system block.
|
|
771
|
+
// Repeated MCP descriptions keep the host <untrusted-content> fence; only the body is encoded.
|
|
772
|
+
// The `tools` list stays raw so {{#has}} still matches the call name.
|
|
773
|
+
const toolInfo = toolNames.map(name => {
|
|
774
|
+
const meta = tools?.get(name);
|
|
775
|
+
return {
|
|
776
|
+
name: escapePromptMetadata(toolPromptNames.get(name) ?? name),
|
|
777
|
+
internalName: escapePromptMetadata(name),
|
|
778
|
+
label: escapePromptMetadata(meta?.label ?? ""),
|
|
779
|
+
description: escapeInventoryDescription(meta?.description ?? ""),
|
|
780
|
+
};
|
|
781
|
+
});
|
|
754
782
|
|
|
755
|
-
const
|
|
783
|
+
const rawSystemPromptCustomization = dedupePromptSource(systemPromptCustomization, [
|
|
756
784
|
resolvedCustomPrompt,
|
|
757
785
|
resolvedAppendPrompt,
|
|
758
786
|
]);
|
|
759
|
-
|
|
787
|
+
// Encode after dedupe so rule matching still sees the file text. The customization
|
|
788
|
+
// template is noEscape; the same encoder used for context files keeps a file from
|
|
789
|
+
// closing <system-prompt-customization> or forging a sibling marker.
|
|
790
|
+
const effectiveSystemPromptCustomization = rawSystemPromptCustomization
|
|
791
|
+
? sanitizeEmbeddedPromptContent(rawSystemPromptCustomization)
|
|
792
|
+
: "";
|
|
793
|
+
const promptSources = [rawSystemPromptCustomization, resolvedCustomPrompt, resolvedAppendPrompt];
|
|
760
794
|
const injectedAlwaysApplyRules = dedupeAlwaysApplyRules(alwaysApplyRules, promptSources);
|
|
761
795
|
|
|
762
796
|
const environment = await logger.time("getEnvironmentInfo", getEnvironmentInfo);
|
|
@@ -771,6 +805,17 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
771
805
|
path: escapePromptMetadata(rule.path),
|
|
772
806
|
content: sanitizeEmbeddedPromptContent(rule.content),
|
|
773
807
|
}));
|
|
808
|
+
// Both rulebook templates compile with noEscape. Encode the fields they interpolate
|
|
809
|
+
// so repository frontmatter cannot close <rules> or the name attribute.
|
|
810
|
+
const sanitizedRulebookRules = (rules ?? []).map(rule => ({
|
|
811
|
+
...rule,
|
|
812
|
+
name: escapePromptMetadata(rule.name),
|
|
813
|
+
description:
|
|
814
|
+
rule.description === undefined
|
|
815
|
+
? undefined
|
|
816
|
+
: escapePromptMetadata(rule.description, { preserveNewlines: true }),
|
|
817
|
+
globs: rule.globs?.map(glob => escapePromptMetadata(glob)),
|
|
818
|
+
}));
|
|
774
819
|
const data = {
|
|
775
820
|
systemPromptCustomization: effectiveSystemPromptCustomization,
|
|
776
821
|
customPrompt: resolvedCustomPrompt,
|
|
@@ -784,7 +829,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
784
829
|
contextFiles: sanitizedContextFiles,
|
|
785
830
|
agentsMdSearch: { files: agentsMdFiles.map(file => escapePromptMetadata(file)) },
|
|
786
831
|
workspaceTree,
|
|
787
|
-
rules:
|
|
832
|
+
rules: sanitizedRulebookRules,
|
|
788
833
|
alwaysApplyRules: sanitizedAlwaysApplyRules,
|
|
789
834
|
cwd: promptCwd,
|
|
790
835
|
intentTracing: !!intentField,
|
package/src/task/executor.ts
CHANGED
|
@@ -76,6 +76,7 @@ import { buildNamedToolChoiceResult } from "../utils/tool-choice";
|
|
|
76
76
|
import type { WorkspaceTree } from "../workspace-tree";
|
|
77
77
|
import { validateAllocatedTaskId } from "./id";
|
|
78
78
|
import { classifyProviderRetryFromTransport, providerNameFromModel } from "./provider-retry-status";
|
|
79
|
+
import { containSubagentRoleBody } from "./role-body";
|
|
79
80
|
import { subprocessToolRegistry } from "./subprocess-tool-registry";
|
|
80
81
|
import { persistTaskTokenLog, taskTokenLogFromUsage } from "./token-log";
|
|
81
82
|
import {
|
|
@@ -2086,14 +2087,15 @@ export async function runSubprocessOnce(options: ExecutorOptions): Promise<Singl
|
|
|
2086
2087
|
promptTemplates: options.promptTemplates,
|
|
2087
2088
|
workspaceTree: options.workspaceTree,
|
|
2088
2089
|
systemPrompt: defaultPrompt => {
|
|
2089
|
-
const
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
assignment: options.assignment ?? task,
|
|
2095
|
-
}),
|
|
2090
|
+
const renderedRole = prompt.render(agent.systemPrompt, {
|
|
2091
|
+
// Typed executionMode wins; assignment text is compatibility-only (#2698 / #2456).
|
|
2092
|
+
ultragoalRedTeam: resolveUltragoalRedTeamActivation({
|
|
2093
|
+
executionMode: options.executionMode,
|
|
2094
|
+
assignment: options.assignment ?? task,
|
|
2096
2095
|
}),
|
|
2096
|
+
});
|
|
2097
|
+
const subagentPrompt = prompt.render(subagentSystemPromptTemplate, {
|
|
2098
|
+
agent: containSubagentRoleBody(renderedRole),
|
|
2097
2099
|
context: options.context?.trim() ?? "",
|
|
2098
2100
|
worktree: worktree ?? "",
|
|
2099
2101
|
outputSchema: normalizedOutputSchema,
|
|
@@ -2209,7 +2211,9 @@ export async function runSubprocessOnce(options: ExecutorOptions): Promise<Singl
|
|
|
2209
2211
|
const parentOwnedToolNames = new Set(["todo_write"]);
|
|
2210
2212
|
const filteredSubagentTools = subagentToolNames.filter(name => !parentOwnedToolNames.has(name));
|
|
2211
2213
|
if (filteredSubagentTools.length !== subagentToolNames.length) {
|
|
2212
|
-
await awaitAbortable(
|
|
2214
|
+
await awaitAbortable(
|
|
2215
|
+
session.setActiveToolsByName(filteredSubagentTools, { explicit: false, persistMCPSelection: false }),
|
|
2216
|
+
);
|
|
2213
2217
|
}
|
|
2214
2218
|
|
|
2215
2219
|
if (!options.preflightProbe && !options.preflightDurable) {
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keep a subagent role body inside the host [ROLE] block.
|
|
3
|
+
*
|
|
4
|
+
* The body is the agent instruction. It is inserted between host [ROLE] and
|
|
5
|
+
* [/ROLE]. A literal framing tag would end that block early. Only the four
|
|
6
|
+
* host framing names are rewritten; other brackets stay literal.
|
|
7
|
+
*
|
|
8
|
+
* One left-to-right scan: a gap run, a slash only when it is present, then
|
|
9
|
+
* one more gap run, then the word. A non-matching "[" does not hide a later tag.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const WORDS = ["completion", "context", "role", "coop"] as const;
|
|
13
|
+
|
|
14
|
+
function isGap(code: number): boolean {
|
|
15
|
+
return (
|
|
16
|
+
code === 0x09 ||
|
|
17
|
+
code === 0x0a ||
|
|
18
|
+
code === 0x0b ||
|
|
19
|
+
code === 0x0c ||
|
|
20
|
+
code === 0x0d ||
|
|
21
|
+
code === 0x20 ||
|
|
22
|
+
code === 0xa0 ||
|
|
23
|
+
code === 0x200b ||
|
|
24
|
+
code === 0x200c ||
|
|
25
|
+
code === 0x200d ||
|
|
26
|
+
code === 0x1680 ||
|
|
27
|
+
(code >= 0x2000 && code <= 0x200a) ||
|
|
28
|
+
code === 0x2028 ||
|
|
29
|
+
code === 0x2029 ||
|
|
30
|
+
code === 0x202f ||
|
|
31
|
+
code === 0x205f ||
|
|
32
|
+
code === 0x3000 ||
|
|
33
|
+
code === 0xfeff
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function wordLengthAt(body: string, index: number): number {
|
|
38
|
+
const slice = body.slice(index, index + 10).toLowerCase();
|
|
39
|
+
for (const word of WORDS) {
|
|
40
|
+
if (!slice.startsWith(word)) continue;
|
|
41
|
+
const after = body.charCodeAt(index + word.length);
|
|
42
|
+
if (Number.isNaN(after) || isGap(after) || after === 0x5d) return word.length;
|
|
43
|
+
}
|
|
44
|
+
return 0;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** End index of a framing tag starting at "[", or -1. */
|
|
48
|
+
function framingTagEnd(body: string, start: number): number {
|
|
49
|
+
let index = start + 1;
|
|
50
|
+
while (index < body.length && isGap(body.charCodeAt(index))) index++;
|
|
51
|
+
if (body.charCodeAt(index) === 0x2f) {
|
|
52
|
+
index++;
|
|
53
|
+
while (index < body.length && isGap(body.charCodeAt(index))) index++;
|
|
54
|
+
}
|
|
55
|
+
const wordLength = wordLengthAt(body, index);
|
|
56
|
+
if (wordLength === 0) return -1;
|
|
57
|
+
index += wordLength;
|
|
58
|
+
while (index < body.length && isGap(body.charCodeAt(index))) index++;
|
|
59
|
+
return body.charCodeAt(index) === 0x5d ? index + 1 : -1;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function containSubagentRoleBody(body: string): string {
|
|
63
|
+
const parts: string[] = [];
|
|
64
|
+
let plainStart = 0;
|
|
65
|
+
let index = 0;
|
|
66
|
+
while (index < body.length) {
|
|
67
|
+
if (body.charCodeAt(index) !== 0x5b) {
|
|
68
|
+
index++;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const end = framingTagEnd(body, index);
|
|
72
|
+
if (end < 0) {
|
|
73
|
+
index++;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (plainStart < index) parts.push(body.slice(plainStart, index));
|
|
77
|
+
parts.push(body.slice(index, end).replaceAll("[", "[").replaceAll("]", "]"));
|
|
78
|
+
index = end;
|
|
79
|
+
plainStart = index;
|
|
80
|
+
}
|
|
81
|
+
if (plainStart === 0) return body;
|
|
82
|
+
if (plainStart < body.length) parts.push(body.slice(plainStart));
|
|
83
|
+
return parts.join("");
|
|
84
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { afterAll, describe, expect, it } from "bun:test";
|
|
2
|
+
import * as fs from "node:fs/promises";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { persistTaskTokenLog, taskTokenLogFromChat } from "./token-log";
|
|
6
|
+
|
|
7
|
+
// Mode bits and final-component symlinks are POSIX. Windows ACLs are out of scope.
|
|
8
|
+
const skipPosix = process.platform === "win32";
|
|
9
|
+
const skipOwnerReadOnly = skipPosix || process.getuid?.() === 0;
|
|
10
|
+
const tempRoots: string[] = [];
|
|
11
|
+
|
|
12
|
+
async function tempDir(): Promise<string> {
|
|
13
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "gjc-token-log-mode-"));
|
|
14
|
+
tempRoots.push(dir);
|
|
15
|
+
return dir;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
afterAll(async () => {
|
|
19
|
+
await Promise.all(tempRoots.splice(0).map(dir => fs.rm(dir, { recursive: true, force: true })));
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
function entry(id: string) {
|
|
23
|
+
return taskTokenLogFromChat(
|
|
24
|
+
{ inputTokens: 1, outputTokens: 2, cachedInputTokens: 0, cacheWriteTokens: 0 },
|
|
25
|
+
{ subagentId: id, turn: 1, at: "2026-01-01T00:00:00.000Z" },
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function modeOf(filePath: string): Promise<number> {
|
|
30
|
+
return (await fs.lstat(filePath)).mode & 0o777;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe("task token log mode", () => {
|
|
34
|
+
it.skipIf(skipPosix)("refuses a symlink token-log.jsonl", async () => {
|
|
35
|
+
const root = await tempDir();
|
|
36
|
+
const outside = path.join(root, "outside.jsonl");
|
|
37
|
+
await fs.writeFile(outside, "kept");
|
|
38
|
+
const dir = path.join(root, "logs");
|
|
39
|
+
await fs.mkdir(dir);
|
|
40
|
+
const link = path.join(dir, "token-log.jsonl");
|
|
41
|
+
await fs.symlink(outside, link);
|
|
42
|
+
|
|
43
|
+
await expect(persistTaskTokenLog(entry("root"), { dir })).rejects.toThrow(/not a regular file/);
|
|
44
|
+
expect(await fs.readFile(outside, "utf8")).toBe("kept");
|
|
45
|
+
expect((await fs.lstat(link)).isSymbolicLink()).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it.skipIf(skipPosix)("refuses a symlink token-log directory", async () => {
|
|
49
|
+
const root = await tempDir();
|
|
50
|
+
const outside = path.join(root, "outside");
|
|
51
|
+
await fs.mkdir(outside);
|
|
52
|
+
const dir = path.join(root, "token-logs");
|
|
53
|
+
await fs.symlink(outside, dir);
|
|
54
|
+
|
|
55
|
+
await expect(persistTaskTokenLog(entry("root"), { dir })).rejects.toThrow(/not a directory/);
|
|
56
|
+
expect(await fs.readdir(outside)).toEqual([]);
|
|
57
|
+
expect((await fs.lstat(dir)).isSymbolicLink()).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it.skipIf(skipPosix)("creates a token log as mode 0600 under a permissive umask", async () => {
|
|
61
|
+
const previous = process.umask(0);
|
|
62
|
+
try {
|
|
63
|
+
const root = await tempDir();
|
|
64
|
+
const dir = path.join(root, "logs");
|
|
65
|
+
await persistTaskTokenLog(entry("root"), { dir });
|
|
66
|
+
expect(await modeOf(dir)).toBe(0o700);
|
|
67
|
+
expect(await modeOf(path.join(dir, "token-log.jsonl"))).toBe(0o600);
|
|
68
|
+
} finally {
|
|
69
|
+
process.umask(previous);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it.skipIf(skipPosix)("tightens an existing mode 0666 token log to 0600", async () => {
|
|
74
|
+
const root = await tempDir();
|
|
75
|
+
const dir = path.join(root, "logs");
|
|
76
|
+
await fs.mkdir(dir, { mode: 0o700 });
|
|
77
|
+
const file = path.join(dir, "token-log.jsonl");
|
|
78
|
+
await fs.writeFile(file, "", { mode: 0o666 });
|
|
79
|
+
await fs.chmod(file, 0o666);
|
|
80
|
+
|
|
81
|
+
await persistTaskTokenLog(entry("root"), { dir });
|
|
82
|
+
|
|
83
|
+
expect(await modeOf(file)).toBe(0o600);
|
|
84
|
+
expect(await fs.readFile(file, "utf8")).toContain('"subagentId":"root"');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it.skipIf(skipPosix)("tightens an existing mode 0777 token-log directory to 0700", async () => {
|
|
88
|
+
const root = await tempDir();
|
|
89
|
+
const dir = path.join(root, "logs");
|
|
90
|
+
await fs.mkdir(dir, { mode: 0o777 });
|
|
91
|
+
await fs.chmod(dir, 0o777);
|
|
92
|
+
|
|
93
|
+
await persistTaskTokenLog(entry("root"), { dir });
|
|
94
|
+
|
|
95
|
+
expect(await modeOf(dir)).toBe(0o700);
|
|
96
|
+
expect(await modeOf(path.join(dir, "token-log.jsonl"))).toBe(0o600);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("still allows a second append to a regular token log", async () => {
|
|
100
|
+
const root = await tempDir();
|
|
101
|
+
const dir = path.join(root, "logs");
|
|
102
|
+
await persistTaskTokenLog(entry("one"), { dir });
|
|
103
|
+
await persistTaskTokenLog(entry("two"), { dir });
|
|
104
|
+
const text = await fs.readFile(path.join(dir, "token-log.jsonl"), "utf8");
|
|
105
|
+
expect(text).toContain('"subagentId":"one"');
|
|
106
|
+
expect(text).toContain('"subagentId":"two"');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it.skipIf(skipPosix)("still allows a token log through a symlinked parent", async () => {
|
|
110
|
+
const root = await tempDir();
|
|
111
|
+
const realParent = path.join(root, "real");
|
|
112
|
+
await fs.mkdir(realParent);
|
|
113
|
+
const via = path.join(root, "via");
|
|
114
|
+
await fs.symlink(realParent, via);
|
|
115
|
+
const dir = path.join(via, "token-logs");
|
|
116
|
+
|
|
117
|
+
await persistTaskTokenLog(entry("via"), { dir });
|
|
118
|
+
|
|
119
|
+
expect(await fs.readFile(path.join(realParent, "token-logs", "token-log.jsonl"), "utf8")).toContain(
|
|
120
|
+
'"subagentId":"via"',
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it.skipIf(skipPosix)("keeps owner mode 0600 when appending a token log", async () => {
|
|
125
|
+
const root = await tempDir();
|
|
126
|
+
const dir = path.join(root, "logs");
|
|
127
|
+
await fs.mkdir(dir, { mode: 0o700 });
|
|
128
|
+
const file = path.join(dir, "token-log.jsonl");
|
|
129
|
+
await fs.writeFile(file, "", { mode: 0o600 });
|
|
130
|
+
|
|
131
|
+
await persistTaskTokenLog(entry("root"), { dir });
|
|
132
|
+
|
|
133
|
+
expect(await modeOf(file)).toBe(0o600);
|
|
134
|
+
expect(await fs.readFile(file, "utf8")).toContain('"subagentId":"root"');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it.skipIf(skipOwnerReadOnly)("still allows a token log in a mode 0300 directory", async () => {
|
|
138
|
+
const root = await tempDir();
|
|
139
|
+
const dir = path.join(root, "logs");
|
|
140
|
+
await fs.mkdir(dir, { mode: 0o700 });
|
|
141
|
+
await fs.chmod(dir, 0o300);
|
|
142
|
+
try {
|
|
143
|
+
await persistTaskTokenLog(entry("root"), { dir });
|
|
144
|
+
expect(await fs.readFile(path.join(dir, "token-log.jsonl"), "utf8")).toContain('"subagentId":"root"');
|
|
145
|
+
} finally {
|
|
146
|
+
await fs.chmod(dir, 0o700);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it.skipIf(skipOwnerReadOnly)("leaves a mode 0400 token log unchanged when the append cannot write", async () => {
|
|
151
|
+
const root = await tempDir();
|
|
152
|
+
const dir = path.join(root, "logs");
|
|
153
|
+
await fs.mkdir(dir, { mode: 0o700 });
|
|
154
|
+
const file = path.join(dir, "token-log.jsonl");
|
|
155
|
+
await fs.writeFile(file, "kept\n", { mode: 0o600 });
|
|
156
|
+
await fs.chmod(file, 0o400);
|
|
157
|
+
|
|
158
|
+
await expect(persistTaskTokenLog(entry("root"), { dir })).rejects.toThrow(/EACCES/);
|
|
159
|
+
expect(await fs.readFile(file, "utf8")).toBe("kept\n");
|
|
160
|
+
expect(await modeOf(file)).toBe(0o400);
|
|
161
|
+
});
|
|
162
|
+
});
|