@gajae-code/coding-agent 0.11.3 → 0.11.5
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 +47 -0
- package/dist/types/cli/notify-cli.d.ts +5 -1
- package/dist/types/cli/worktree-cli.d.ts +8 -25
- package/dist/types/commands/gc.d.ts +4 -0
- package/dist/types/commands/worktree.d.ts +7 -32
- package/dist/types/config/settings-schema.d.ts +20 -0
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +28 -0
- package/dist/types/gjc-runtime/gc-runtime.d.ts +9 -0
- package/dist/types/gjc-runtime/launch-tmux.d.ts +3 -0
- package/dist/types/gjc-runtime/managed-owner-admission.d.ts +42 -0
- package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +41 -0
- package/dist/types/gjc-runtime/state-argv.d.ts +38 -0
- package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +9 -0
- package/dist/types/gjc-runtime/ultragoal-owner-loss-recovery.d.ts +94 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +1 -0
- package/dist/types/harness-control-plane/owner.d.ts +3 -1
- package/dist/types/internal-urls/local-protocol.d.ts +18 -0
- package/dist/types/main.d.ts +2 -2
- package/dist/types/modes/components/ansi-display-validator.d.ts +2 -0
- package/dist/types/modes/components/custom-editor.d.ts +8 -3
- package/dist/types/modes/components/notifications-settings-editor.d.ts +4 -0
- package/dist/types/modes/components/session-observer-overlay.d.ts +3 -1
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- package/dist/types/modes/components/tool-transcript-format.d.ts +77 -0
- package/dist/types/modes/components/transcript-viewer-overlay.d.ts +3 -0
- package/dist/types/modes/controllers/goal-mode-controller.d.ts +8 -1
- package/dist/types/modes/controllers/input-controller.d.ts +8 -2
- package/dist/types/modes/controllers/selector-controller.d.ts +15 -0
- package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +3 -0
- package/dist/types/modes/shared/agent-wire/workflow-gate-types.d.ts +16 -0
- package/dist/types/plan-mode/approved-plan.d.ts +1 -0
- package/dist/types/runtime-mcp/manager.d.ts +2 -0
- package/dist/types/runtime-mcp/oauth-discovery.d.ts +12 -1
- package/dist/types/sdk/broker/broker.d.ts +1 -0
- package/dist/types/sdk/broker/discovery.d.ts +10 -0
- package/dist/types/sdk/broker/ensure.d.ts +24 -1
- package/dist/types/sdk/broker/internal.d.ts +3 -0
- package/dist/types/sdk/broker/session-index.d.ts +12 -0
- package/dist/types/sdk/broker/state-version.d.ts +2 -1
- package/dist/types/sdk/bus/chat-daemon-cli.d.ts +3 -1
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +39 -4
- package/dist/types/sdk/bus/config-commands.d.ts +8 -0
- package/dist/types/sdk/bus/config.d.ts +22 -0
- package/dist/types/sdk/bus/index.d.ts +162 -2
- package/dist/types/sdk/bus/notification-service.d.ts +71 -8
- package/dist/types/sdk/bus/session-control.d.ts +11 -8
- package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +5 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +6 -3
- package/dist/types/sdk/bus/telegram-daemon-control.d.ts +23 -4
- package/dist/types/sdk/bus/telegram-daemon.d.ts +214 -20
- package/dist/types/sdk/session.d.ts +16 -1
- package/dist/types/secrets/index.d.ts +4 -2
- package/dist/types/secrets/obfuscator.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +31 -2
- package/dist/types/session/artifacts.d.ts +30 -8
- package/dist/types/session/internal/managed-session-scope.d.ts +7 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +63 -6
- package/dist/types/session/session-manager.d.ts +88 -18
- package/dist/types/session/session-storage.d.ts +27 -0
- package/dist/types/session/streaming-output.d.ts +4 -0
- package/dist/types/task/executor.d.ts +9 -0
- package/dist/types/tool-discovery/tool-index.d.ts +2 -0
- package/dist/types/tools/ask.d.ts +87 -12
- package/dist/types/tools/bash-skill-urls.d.ts +2 -2
- package/dist/types/tools/browser/screenshot-format.d.ts +8 -0
- package/dist/types/tools/browser/tab-protocol.d.ts +11 -0
- package/dist/types/tools/browser/tab-supervisor.d.ts +1 -1
- package/dist/types/tools/browser/tab-worker-path-resolver.d.ts +2 -0
- package/dist/types/tools/browser/tab-worker-smoke.d.ts +16 -0
- package/dist/types/tools/index.d.ts +4 -0
- package/dist/types/tools/path-utils.d.ts +9 -14
- package/dist/types/tools/render-utils.d.ts +1 -8
- package/dist/types/tools/resource-gc.d.ts +14 -0
- package/dist/types/utils/image-loading.d.ts +23 -0
- package/dist/types/utils/image-resize.d.ts +2 -0
- package/dist/types/utils/pasted-image-loading.d.ts +39 -0
- package/dist/types/utils/pasted-image-path.d.ts +21 -19
- package/dist/types/web/scrapers/html-to-markdown.d.ts +5 -0
- package/dist/types/web/scrapers/types.d.ts +1 -5
- package/package.json +25 -9
- package/scripts/generate-sdk-operation-inventory.ts +2 -0
- package/scripts/run-threat-model-check.ts +60 -0
- package/src/cli/notify-cli.ts +40 -18
- package/src/cli/worktree-cli.ts +9 -287
- package/src/cli.ts +10 -9
- package/src/commands/gc.ts +11 -1
- package/src/commands/sdk.ts +10 -4
- package/src/commands/worktree.ts +10 -53
- package/src/config/model-profiles.ts +15 -15
- package/src/config/settings-schema.ts +17 -0
- package/src/daemon/runtime.ts +2 -2
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +70 -49
- package/src/defaults/gjc/skills/ralplan/SKILL.md +0 -6
- package/src/defaults/gjc/skills/team/SKILL.md +7 -9
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +0 -2
- package/src/eval/js/shared/runtime.ts +1 -1
- package/src/eval/py/index.ts +2 -2
- package/src/export/html/template.generated.ts +1 -1
- package/src/extensibility/plugins/marketplace/manager.ts +94 -10
- package/src/gjc-runtime/deep-interview-recorder.ts +7 -0
- package/src/gjc-runtime/deep-interview-runtime.ts +10 -0
- package/src/gjc-runtime/deep-interview-state.ts +127 -3
- package/src/gjc-runtime/gc-render.ts +19 -1
- package/src/gjc-runtime/gc-runtime.ts +68 -3
- package/src/gjc-runtime/launch-tmux.ts +61 -6
- package/src/gjc-runtime/managed-owner-admission.ts +352 -0
- package/src/gjc-runtime/managed-owner-supervisor.ts +162 -0
- package/src/gjc-runtime/state-argv.ts +292 -0
- package/src/gjc-runtime/state-runtime.ts +80 -222
- package/src/gjc-runtime/team-runtime.ts +1 -1
- package/src/gjc-runtime/tmux-owner-isolation.ts +117 -0
- package/src/gjc-runtime/ultragoal-owner-loss-recovery.ts +328 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +14 -0
- package/src/harness-control-plane/owner.ts +59 -3
- package/src/hindsight/client.ts +65 -2
- package/src/internal-urls/agent-protocol.ts +76 -5
- package/src/internal-urls/docs-index.generated.ts +7 -6
- package/src/internal-urls/local-protocol.ts +416 -18
- package/src/lsp/index.ts +4 -88
- package/src/main.ts +37 -13
- package/src/modes/acp/acp-agent.ts +8 -0
- package/src/modes/components/ansi-display-validator.ts +54 -0
- package/src/modes/components/custom-editor.ts +103 -32
- package/src/modes/components/custom-provider-wizard.ts +26 -1
- package/src/modes/components/notifications-settings-editor.ts +26 -2
- package/src/modes/components/session-observer-overlay.ts +40 -37
- package/src/modes/components/settings-selector.ts +3 -1
- package/src/modes/components/tool-transcript-format.ts +393 -0
- package/src/modes/components/transcript-viewer-overlay.ts +63 -1
- package/src/modes/controllers/goal-mode-controller.ts +20 -1
- package/src/modes/controllers/input-controller.ts +98 -37
- package/src/modes/controllers/plan-mode-controller.ts +2 -0
- package/src/modes/controllers/runtime-mcp-command-controller.ts +3 -0
- package/src/modes/controllers/selector-controller.ts +71 -9
- package/src/modes/interactive-mode.ts +14 -2
- package/src/modes/shared/agent-wire/deep-interview-gate.ts +31 -1
- package/src/modes/shared/agent-wire/workflow-gate-broker.ts +17 -1
- package/src/modes/shared/agent-wire/workflow-gate-schema.ts +4 -2
- package/src/modes/shared/agent-wire/workflow-gate-types.ts +120 -3
- package/src/plan-mode/approved-plan.ts +3 -1
- package/src/prompts/agents/architect.md +1 -0
- package/src/prompts/agents/critic.md +1 -0
- package/src/prompts/system/custom-system-prompt.md +1 -1
- package/src/prompts/system/system-prompt.md +2 -1
- package/src/prompts/tools/lsp.md +3 -3
- package/src/runtime-mcp/manager.ts +16 -1
- package/src/runtime-mcp/oauth-discovery.ts +116 -26
- package/src/sdk/broker/broker.ts +139 -32
- package/src/sdk/broker/discovery.ts +125 -0
- package/src/sdk/broker/ensure.ts +103 -19
- package/src/sdk/broker/internal.ts +10 -0
- package/src/sdk/broker/session-index.ts +309 -38
- package/src/sdk/broker/state-version.ts +5 -2
- package/src/sdk/bus/chat-daemon-cli.ts +72 -10
- package/src/sdk/bus/chat-daemon-control.ts +526 -130
- package/src/sdk/bus/chat-daemon-runtime.ts +1 -0
- package/src/sdk/bus/config-commands.ts +22 -0
- package/src/sdk/bus/config.ts +37 -0
- package/src/sdk/bus/index.ts +384 -92
- package/src/sdk/bus/lifecycle-control-runtime.ts +53 -3
- package/src/sdk/bus/notification-service.ts +284 -62
- package/src/sdk/bus/recent-activity.ts +14 -5
- package/src/sdk/bus/session-control.ts +150 -44
- package/src/sdk/bus/telegram-daemon-cli.ts +20 -14
- package/src/sdk/bus/telegram-daemon-contract.ts +6 -3
- package/src/sdk/bus/telegram-daemon-control.ts +294 -76
- package/src/sdk/bus/telegram-daemon.ts +1842 -281
- package/src/sdk/protocol/operation-inventory.generated.json +11 -0
- package/src/sdk/session.ts +155 -27
- package/src/secrets/index.ts +53 -10
- package/src/secrets/obfuscator.ts +21 -18
- package/src/secrets/regex.ts +2 -0
- package/src/session/agent-session.ts +495 -253
- package/src/session/artifacts.ts +188 -23
- package/src/session/context-estimation.ts +7 -2
- package/src/session/internal/managed-session-scope.ts +215 -39
- package/src/session/internal/managed-session-storage.ts +1152 -46
- package/src/session/session-manager.ts +1055 -148
- package/src/session/session-storage.ts +109 -11
- package/src/session/streaming-output.ts +6 -0
- package/src/task/executor.ts +71 -26
- package/src/task/index.ts +63 -20
- package/src/tool-discovery/tool-index.ts +18 -0
- package/src/tools/ask.ts +329 -57
- package/src/tools/bash-allowed-prefixes.ts +79 -29
- package/src/tools/bash-skill-urls.ts +14 -3
- package/src/tools/bash.ts +2 -2
- package/src/tools/browser/launch.ts +3 -1
- package/src/tools/browser/readable.ts +1 -1
- package/src/tools/browser/screenshot-format.ts +34 -0
- package/src/tools/browser/tab-protocol.ts +3 -0
- package/src/tools/browser/tab-supervisor.ts +125 -122
- package/src/tools/browser/tab-worker-entry.ts +33 -14
- package/src/tools/browser/tab-worker-path-resolver.ts +67 -0
- package/src/tools/browser/tab-worker-smoke.ts +87 -0
- package/src/tools/browser/tab-worker.ts +7 -5
- package/src/tools/index.ts +4 -0
- package/src/tools/path-utils.ts +42 -35
- package/src/tools/plan-mode-guard.ts +1 -0
- package/src/tools/read.ts +7 -1
- package/src/tools/render-utils.ts +1 -27
- package/src/tools/resource-gc.ts +123 -34
- package/src/tools/sqlite-reader.ts +3 -2
- package/src/tools/subagent.ts +2 -0
- package/src/utils/image-loading.ts +85 -34
- package/src/utils/image-resize.ts +16 -9
- package/src/utils/pasted-image-loading.ts +514 -0
- package/src/utils/pasted-image-path.ts +223 -98
- package/src/web/scrapers/html-to-markdown.ts +68 -0
- package/src/web/scrapers/readthedocs.ts +1 -1
- package/src/web/scrapers/types.ts +1 -68
- package/src/web/search/providers/openai-compatible.ts +76 -23
|
@@ -2809,6 +2809,17 @@
|
|
|
2809
2809
|
"testIds": "not_applicable"
|
|
2810
2810
|
}
|
|
2811
2811
|
},
|
|
2812
|
+
{
|
|
2813
|
+
"sourceId": "agent_session:promoteRecoveryHydrationAfterOwnershipReadyFence",
|
|
2814
|
+
"sourceFile": "packages/coding-agent/src/session/agent-session.ts",
|
|
2815
|
+
"sourceKind": "agent_session",
|
|
2816
|
+
"decision": "exclude",
|
|
2817
|
+
"rationale": "internal owner-recovery authority transition after a durable writer fence, never a user-facing SDK operation",
|
|
2818
|
+
"exclusionMetadata": {
|
|
2819
|
+
"adapterMappings": "not_applicable",
|
|
2820
|
+
"testIds": "not_applicable"
|
|
2821
|
+
}
|
|
2822
|
+
},
|
|
2812
2823
|
{
|
|
2813
2824
|
"sourceId": "agent_session:continuePersistedHistory",
|
|
2814
2825
|
"sourceFile": "packages/coding-agent/src/session/agent-session.ts",
|
package/src/sdk/session.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
ThinkingLevel,
|
|
11
11
|
} from "@gajae-code/agent-core";
|
|
12
12
|
import {
|
|
13
|
+
type AssistantMessage,
|
|
13
14
|
type AuthCredentialSelector,
|
|
14
15
|
type CredentialDisabledEvent,
|
|
15
16
|
type Message,
|
|
@@ -17,6 +18,7 @@ import {
|
|
|
17
18
|
type ProviderSessionState,
|
|
18
19
|
type SimpleStreamOptions,
|
|
19
20
|
streamSimple,
|
|
21
|
+
type ToolResultMessage,
|
|
20
22
|
} from "@gajae-code/ai";
|
|
21
23
|
import {
|
|
22
24
|
getOpenAICodexTransportDetails,
|
|
@@ -88,7 +90,7 @@ import { loadActiveSubskillTools } from "../extensibility/gjc-plugins/tools";
|
|
|
88
90
|
import { loadSkills, type Skill, type SkillWarning, setActiveSkills } from "../extensibility/skills";
|
|
89
91
|
import type { FileSlashCommand } from "../extensibility/slash-commands";
|
|
90
92
|
import type { HindsightSessionState } from "../hindsight/state";
|
|
91
|
-
import { LocalProtocolHandler, type LocalProtocolOptions } from "../internal-urls";
|
|
93
|
+
import { initializeLocalRoot, LocalProtocolHandler, type LocalProtocolOptions } from "../internal-urls";
|
|
92
94
|
import { resolveMemoryBackend } from "../memory-backend";
|
|
93
95
|
import asyncResultTemplate from "../prompts/tools/async-result.md" with { type: "text" };
|
|
94
96
|
import { AgentRegistry, MAIN_AGENT_ID } from "../registry/agent-registry";
|
|
@@ -105,10 +107,11 @@ import { NotificationSessionController } from "../sdk/bus/session-control";
|
|
|
105
107
|
import { shouldHostSdk } from "../sdk/host";
|
|
106
108
|
import {
|
|
107
109
|
collectEnvSecrets,
|
|
110
|
+
createSecretObfuscator,
|
|
108
111
|
deobfuscateSessionContext,
|
|
109
112
|
loadSecrets,
|
|
110
113
|
obfuscateMessages,
|
|
111
|
-
SecretObfuscator,
|
|
114
|
+
type SecretObfuscator,
|
|
112
115
|
} from "../secrets";
|
|
113
116
|
import { AgentSession, type ForkContextSeed } from "../session/agent-session";
|
|
114
117
|
import { resolveAuthBrokerConfig } from "../session/auth-broker-config";
|
|
@@ -126,7 +129,7 @@ import {
|
|
|
126
129
|
} from "../system-prompt";
|
|
127
130
|
import { AgentOutputManager } from "../task/output-manager";
|
|
128
131
|
import { parseThinkingLevel, resolveThinkingLevelForModel, toReasoningEffort } from "../thinking";
|
|
129
|
-
import { isMCPBridgeTool } from "../tool-discovery/tool-index";
|
|
132
|
+
import { isMCPBridgeTool, selectRestorableDiscoveredBuiltinToolNames } from "../tool-discovery/tool-index";
|
|
130
133
|
import {
|
|
131
134
|
applyConfiguredSearchTimeout,
|
|
132
135
|
BashTool,
|
|
@@ -221,6 +224,57 @@ function buildAsyncResultBatchMessage(entries: AsyncResultEntry[]): CustomMessag
|
|
|
221
224
|
};
|
|
222
225
|
}
|
|
223
226
|
|
|
227
|
+
/**
|
|
228
|
+
* Reconcile a resumed transcript that ends on an unpaired tool call.
|
|
229
|
+
*
|
|
230
|
+
* When a subagent finishes by calling `yield` (or any turn is torn down right
|
|
231
|
+
* after a tool executes), the terminating abort can land before the tool
|
|
232
|
+
* result is persisted, leaving the saved session ending on an assistant
|
|
233
|
+
* `toolCall` with no matching `toolResult`. Replaying that history verbatim on
|
|
234
|
+
* resume produces an invalid provider request (a tool_use not followed by a
|
|
235
|
+
* tool_result) and the resumed turn fails immediately. Synthesize a placeholder
|
|
236
|
+
* result for any such trailing unpaired tool call so a resumed session always
|
|
237
|
+
* starts from a valid, paired history. No-op for well-formed transcripts.
|
|
238
|
+
*/
|
|
239
|
+
export function reconcileTrailingToolCalls(messages: AgentMessage[]): AgentMessage[] {
|
|
240
|
+
let lastAssistantIdx = -1;
|
|
241
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
242
|
+
if (messages[i].role === "assistant") {
|
|
243
|
+
lastAssistantIdx = i;
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if (lastAssistantIdx === -1) return messages;
|
|
248
|
+
const lastAssistant = messages[lastAssistantIdx] as AssistantMessage;
|
|
249
|
+
const toolCalls = lastAssistant.content.filter(
|
|
250
|
+
(part): part is Extract<AssistantMessage["content"][number], { type: "toolCall" }> => part.type === "toolCall",
|
|
251
|
+
);
|
|
252
|
+
if (toolCalls.length === 0) return messages;
|
|
253
|
+
const satisfied = new Set<string>();
|
|
254
|
+
for (let i = lastAssistantIdx + 1; i < messages.length; i++) {
|
|
255
|
+
const message = messages[i];
|
|
256
|
+
if (message.role === "toolResult") satisfied.add(message.toolCallId);
|
|
257
|
+
}
|
|
258
|
+
const missing = toolCalls.filter(toolCall => !satisfied.has(toolCall.id));
|
|
259
|
+
if (missing.length === 0) return messages;
|
|
260
|
+
const now = Date.now();
|
|
261
|
+
const synthesized: ToolResultMessage[] = missing.map(toolCall => ({
|
|
262
|
+
role: "toolResult",
|
|
263
|
+
toolCallId: toolCall.id,
|
|
264
|
+
toolName: toolCall.name,
|
|
265
|
+
content: [
|
|
266
|
+
{
|
|
267
|
+
type: "text",
|
|
268
|
+
text: "Tool result was not persisted before the previous turn ended; synthesized on resume to keep tool_use/tool_result pairing valid.",
|
|
269
|
+
},
|
|
270
|
+
],
|
|
271
|
+
details: {},
|
|
272
|
+
isError: false,
|
|
273
|
+
timestamp: now,
|
|
274
|
+
}));
|
|
275
|
+
return [...messages, ...synthesized];
|
|
276
|
+
}
|
|
277
|
+
|
|
224
278
|
function buildMcpNotificationBatchMessage(entries: McpNotificationEntry[]): AgentMessage | null {
|
|
225
279
|
const resources: McpNotificationEntry[] = [];
|
|
226
280
|
const seen = new Set<string>();
|
|
@@ -392,6 +446,8 @@ export interface CreateAgentSessionOptions {
|
|
|
392
446
|
notificationHostModeSupported?: boolean;
|
|
393
447
|
/** Whether this host mode can own the root SDK endpoint. Default: true. */
|
|
394
448
|
sdkHostModeSupported?: boolean;
|
|
449
|
+
/** Override configured Discord/Slack daemon readiness, primarily for embedded hosts and deterministic tests. */
|
|
450
|
+
ensureNotificationProviderDaemon?: (provider: "discord" | "slack", settings: Settings) => Promise<unknown>;
|
|
395
451
|
|
|
396
452
|
/**
|
|
397
453
|
* Opt-in OpenTelemetry instrumentation forwarded to the underlying Agent.
|
|
@@ -926,6 +982,7 @@ const MCP_TOOLS_ONLY_MANAGER_SUBSESSION_ERROR = "tools-only MCP managers cannot
|
|
|
926
982
|
const MCP_CONFIG_PATH_SUBSESSION_ERROR = "mcpConfigPath cannot be used in sub-sessions";
|
|
927
983
|
const MAX_EXACT_MCP_TOOL_COLLISION_NAMES = 10;
|
|
928
984
|
const MAX_EXACT_MCP_TOOL_NAME_LENGTH = 100;
|
|
985
|
+
const pluginMcpManagerServers = new WeakMap<MCPManager, ReadonlySet<string>>();
|
|
929
986
|
|
|
930
987
|
class ExactMcpToolNameCollisionError extends Error {
|
|
931
988
|
constructor(toolNames: Iterable<string>) {
|
|
@@ -1114,8 +1171,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1114
1171
|
const sessionManager =
|
|
1115
1172
|
options.sessionManager ??
|
|
1116
1173
|
(await logger.time("sessionManager", async () => {
|
|
1117
|
-
|
|
1118
|
-
return SessionManager.create(cwd, sessionDir);
|
|
1174
|
+
return SessionManager.create(cwd, SessionManager.managedDestination(cwd, agentDir));
|
|
1119
1175
|
}));
|
|
1120
1176
|
const logicalSessionId = sessionManager.getSessionId();
|
|
1121
1177
|
const providerSessionId = options.providerSessionId ?? options.forkContextSeed?.cacheIdentity ?? logicalSessionId;
|
|
@@ -1166,7 +1222,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1166
1222
|
const envEntries = collectEnvSecrets();
|
|
1167
1223
|
const allEntries = [...envEntries, ...fileEntries];
|
|
1168
1224
|
if (allEntries.length > 0) {
|
|
1169
|
-
obfuscator =
|
|
1225
|
+
obfuscator = createSecretObfuscator(allEntries);
|
|
1170
1226
|
}
|
|
1171
1227
|
}
|
|
1172
1228
|
const secretsEnabled = obfuscator?.hasSecrets() === true;
|
|
@@ -1427,6 +1483,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1427
1483
|
return sessionManager.getCwd();
|
|
1428
1484
|
},
|
|
1429
1485
|
hasUI: options.hasUI ?? false,
|
|
1486
|
+
workflowGateEligible: true,
|
|
1430
1487
|
enableLsp,
|
|
1431
1488
|
get hasEditTool() {
|
|
1432
1489
|
const requestedToolNames = options.toolNames
|
|
@@ -1449,6 +1506,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1449
1506
|
trackEvalExecution: (execution, abortController) =>
|
|
1450
1507
|
session ? session.trackEvalExecution(execution, abortController) : execution,
|
|
1451
1508
|
getSessionId: () => sessionManager.getSessionId?.() ?? null,
|
|
1509
|
+
isManagedSessionDestination: () => sessionManager.isManagedDestination(),
|
|
1452
1510
|
getActiveSkillState: () => session?.getActiveSkillState(),
|
|
1453
1511
|
getActiveSkillPhase: () => session?.getActiveSkillPhase(),
|
|
1454
1512
|
getHindsightSessionState: () => session?.getHindsightSessionState(),
|
|
@@ -1538,6 +1596,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1538
1596
|
if (asyncJobManager) AsyncJobManager.setInstance(asyncJobManager);
|
|
1539
1597
|
}
|
|
1540
1598
|
if (options.localProtocolOptions) {
|
|
1599
|
+
await initializeLocalRoot(options.localProtocolOptions);
|
|
1541
1600
|
disposeLocalProtocolOverride = LocalProtocolHandler.installOverride(options.localProtocolOptions);
|
|
1542
1601
|
}
|
|
1543
1602
|
toolSession.getArtifactsDir = getArtifactsDir;
|
|
@@ -1559,6 +1618,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1559
1618
|
const explicitMcpConfigPath = !isCanonicalSubSession && !options.mcpManager ? options.mcpConfigPath : undefined;
|
|
1560
1619
|
const customTools: CustomTool[] = [];
|
|
1561
1620
|
const exactMcpToolNames: string[] = [];
|
|
1621
|
+
const pluginMcpToolNames: string[] = [];
|
|
1562
1622
|
|
|
1563
1623
|
// Add image tools when the active model or configured image providers can generate images.
|
|
1564
1624
|
const imageGenTools = await logger.time("getImageGenTools", () => getImageGenTools(modelRegistry, model));
|
|
@@ -1670,6 +1730,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1670
1730
|
mcpManager = owned;
|
|
1671
1731
|
ownsMcpManager = true;
|
|
1672
1732
|
customTools.push(...(result.tools as CustomTool[]));
|
|
1733
|
+
pluginMcpManagerServers.set(owned, new Set(result.connectedServers));
|
|
1734
|
+
owned.sealConnectionSet();
|
|
1735
|
+
pluginMcpToolNames.push(...result.tools.map(tool => tool.name));
|
|
1673
1736
|
} else {
|
|
1674
1737
|
await owned.disconnectAll().catch(() => {});
|
|
1675
1738
|
}
|
|
@@ -1694,8 +1757,16 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1694
1757
|
try {
|
|
1695
1758
|
const inheritedTools = inherited.getTools();
|
|
1696
1759
|
if (inheritedTools.length > 0) customTools.push(...(inheritedTools as CustomTool[]));
|
|
1760
|
+
const pluginServers = pluginMcpManagerServers.get(inherited);
|
|
1761
|
+
if (pluginServers) {
|
|
1762
|
+
pluginMcpToolNames.push(
|
|
1763
|
+
...inheritedTools
|
|
1764
|
+
.filter(tool => tool.mcpServerName !== undefined && pluginServers.has(tool.mcpServerName))
|
|
1765
|
+
.map(tool => tool.name),
|
|
1766
|
+
);
|
|
1767
|
+
}
|
|
1697
1768
|
} catch (error) {
|
|
1698
|
-
logger.warn("Failed to inherit
|
|
1769
|
+
logger.warn("Failed to inherit MCP tools in subagent", { error });
|
|
1699
1770
|
}
|
|
1700
1771
|
}
|
|
1701
1772
|
}
|
|
@@ -1777,6 +1848,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1777
1848
|
controller: notificationSessionController,
|
|
1778
1849
|
spawnedByGjc,
|
|
1779
1850
|
sdkHostModeSupported: options.sdkHostModeSupported,
|
|
1851
|
+
ensureProviderDaemon: options.ensureNotificationProviderDaemon,
|
|
1780
1852
|
runEphemeralTurn: async (promptText, signal) => {
|
|
1781
1853
|
if (!session) throw new Error("Ephemeral turns are unavailable.");
|
|
1782
1854
|
const { replyText } = await session.runEphemeralTurn({ promptText, signal });
|
|
@@ -2121,15 +2193,17 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2121
2193
|
};
|
|
2122
2194
|
|
|
2123
2195
|
const toolNamesFromRegistry = Array.from(toolRegistry.keys());
|
|
2124
|
-
const
|
|
2196
|
+
const hasExplicitToolNames = options.toolNames !== undefined;
|
|
2197
|
+
const requestedToolNames = hasExplicitToolNames
|
|
2125
2198
|
? [
|
|
2126
2199
|
...new Set([
|
|
2127
|
-
...options.toolNames
|
|
2200
|
+
...options.toolNames!.map(name => name.toLowerCase()),
|
|
2128
2201
|
...(settings.get("goal.enabled") ? ["goal"] : []),
|
|
2129
2202
|
]),
|
|
2130
2203
|
]
|
|
2131
2204
|
: toolNamesFromRegistry;
|
|
2132
2205
|
const normalizedRequested = requestedToolNames.filter(name => toolRegistry.has(name));
|
|
2206
|
+
const explicitRequestedToolNames = hasExplicitToolNames ? normalizedRequested : [];
|
|
2133
2207
|
const requestedToolNameSet = new Set(normalizedRequested);
|
|
2134
2208
|
// Normalize the user-facing mcp.discoveryMode alias once at session construction.
|
|
2135
2209
|
const toolsDiscoveryModeSetting = settings.get("tools.discoveryMode");
|
|
@@ -2152,12 +2226,17 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2152
2226
|
const discoveryDefaultServerToolNames: string[] = [];
|
|
2153
2227
|
let initialSelectedMCPToolNames: string[] = [];
|
|
2154
2228
|
let defaultSelectedMCPToolNames: string[] = [];
|
|
2229
|
+
let initialBaselineDiscoveredBuiltinToolNames: string[] = [];
|
|
2230
|
+
let initialSelectedDiscoveredBuiltinToolNames: string[] = [];
|
|
2231
|
+
let hasExplicitMCPToolSelection = false;
|
|
2232
|
+
let hasExplicitDiscoveredBuiltinToolSelection = false;
|
|
2233
|
+
let explicitlyRequestedDiscoveredBuiltinToolNames: string[] = [];
|
|
2155
2234
|
if (mcpDiscoveryEnabled) {
|
|
2156
2235
|
const defaultServerNames = new Set(settings.get("mcp.discoveryDefaultServers") ?? []);
|
|
2157
2236
|
for (const tool of toolRegistry.values()) {
|
|
2158
2237
|
if (!isMCPBridgeTool(tool)) continue;
|
|
2159
2238
|
discoverableMCPToolNames.add(tool.name);
|
|
2160
|
-
if (
|
|
2239
|
+
if (explicitRequestedToolNames.includes(tool.name)) {
|
|
2161
2240
|
explicitlyRequestedMCPToolNames.push(tool.name);
|
|
2162
2241
|
}
|
|
2163
2242
|
const serverName = (tool as AgentTool & { mcpServerName?: string }).mcpServerName;
|
|
@@ -2166,21 +2245,34 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2166
2245
|
}
|
|
2167
2246
|
}
|
|
2168
2247
|
}
|
|
2248
|
+
const mandatoryMCPToolNameSet = new Set(pluginMcpToolNames);
|
|
2249
|
+
const selectableExplicitMCPToolNames = explicitlyRequestedMCPToolNames.filter(
|
|
2250
|
+
name => !mandatoryMCPToolNameSet.has(name),
|
|
2251
|
+
);
|
|
2169
2252
|
let initialToolNames = [...initialRequestedActiveToolNames];
|
|
2170
2253
|
if (mcpDiscoveryEnabled) {
|
|
2171
|
-
const restoredSelectedMCPToolNames = existingSession.selectedMCPToolNames.filter(
|
|
2172
|
-
toolRegistry.has(name),
|
|
2254
|
+
const restoredSelectedMCPToolNames = existingSession.selectedMCPToolNames.filter(
|
|
2255
|
+
name => toolRegistry.has(name) && !mandatoryMCPToolNameSet.has(name),
|
|
2173
2256
|
);
|
|
2257
|
+
if (
|
|
2258
|
+
existingSession.hasPersistedMCPToolSelection &&
|
|
2259
|
+
restoredSelectedMCPToolNames.length !== existingSession.selectedMCPToolNames.length
|
|
2260
|
+
) {
|
|
2261
|
+
sessionManager.appendMCPToolSelection(restoredSelectedMCPToolNames);
|
|
2262
|
+
}
|
|
2174
2263
|
defaultSelectedMCPToolNames = [
|
|
2175
2264
|
...new Set([
|
|
2176
2265
|
...discoveryDefaultServerToolNames,
|
|
2177
|
-
...explicitlyRequestedMCPToolNames,
|
|
2178
2266
|
...(explicitMcpConfigPath !== undefined ? exactMcpToolNames : []),
|
|
2179
2267
|
]),
|
|
2180
2268
|
];
|
|
2269
|
+
hasExplicitMCPToolSelection =
|
|
2270
|
+
hasExplicitToolNames && (options.toolNames!.length === 0 || selectableExplicitMCPToolNames.length > 0);
|
|
2181
2271
|
initialSelectedMCPToolNames = existingSession.hasPersistedMCPToolSelection
|
|
2182
2272
|
? restoredSelectedMCPToolNames
|
|
2183
|
-
:
|
|
2273
|
+
: hasExplicitMCPToolSelection
|
|
2274
|
+
? selectableExplicitMCPToolNames
|
|
2275
|
+
: defaultSelectedMCPToolNames;
|
|
2184
2276
|
initialToolNames = [
|
|
2185
2277
|
...new Set([
|
|
2186
2278
|
...initialRequestedActiveToolNames.filter(name => !discoverableMCPToolNames.has(name)),
|
|
@@ -2189,13 +2281,17 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2189
2281
|
];
|
|
2190
2282
|
}
|
|
2191
2283
|
|
|
2192
|
-
// Custom
|
|
2284
|
+
// Custom, extension-registered, and plugin-bundle MCP tools are always
|
|
2285
|
+
// included regardless of the caller's built-in tool filter. Plugin MCPs
|
|
2286
|
+
// remain always-on even when generic MCP discovery is disabled.
|
|
2193
2287
|
const alwaysInclude: string[] = [
|
|
2194
2288
|
...(options.customTools?.map(t => (isCustomTool(t) ? t.name : t.name)) ?? []),
|
|
2195
2289
|
...registeredTools.filter(t => !t.definition.defaultInactive).map(t => t.definition.name),
|
|
2290
|
+
...pluginMcpToolNames,
|
|
2196
2291
|
];
|
|
2292
|
+
const pluginMcpToolNameSet = new Set(pluginMcpToolNames);
|
|
2197
2293
|
for (const name of alwaysInclude) {
|
|
2198
|
-
if (mcpDiscoveryEnabled && discoverableMCPToolNames.has(name)) {
|
|
2294
|
+
if (mcpDiscoveryEnabled && discoverableMCPToolNames.has(name) && !pluginMcpToolNameSet.has(name)) {
|
|
2199
2295
|
continue;
|
|
2200
2296
|
}
|
|
2201
2297
|
if (toolRegistry.has(name) && !initialToolNames.includes(name)) {
|
|
@@ -2208,19 +2304,39 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2208
2304
|
// The model finds them via search_tool_bm25 and activates them on demand.
|
|
2209
2305
|
if (effectiveDiscoveryMode === "all") {
|
|
2210
2306
|
const essentialBuiltinNames = new Set(computeEssentialBuiltinNames(settings));
|
|
2211
|
-
const
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
const
|
|
2215
|
-
initialToolNames = initialToolNames.filter(name => {
|
|
2307
|
+
const allowedDiscoveredBuiltinNames = options.discoverableToolAllowedNames
|
|
2308
|
+
? new Set(options.discoverableToolAllowedNames.map(name => name.toLowerCase()))
|
|
2309
|
+
: undefined;
|
|
2310
|
+
const baselineInitialToolNames = initialToolNames.filter(name => {
|
|
2216
2311
|
const tool = toolRegistry.get(name);
|
|
2217
2312
|
if (!tool?.loadMode) return true; // not a built-in — leave MCP/custom/extension to existing logic
|
|
2218
2313
|
if (tool.loadMode === "essential") return true;
|
|
2219
|
-
|
|
2220
|
-
if (explicitlyRequestedToolNames.has(name)) return true;
|
|
2221
|
-
if (restoredDiscoveredNames.has(name)) return true;
|
|
2222
|
-
return false;
|
|
2314
|
+
return essentialBuiltinNames.has(name);
|
|
2223
2315
|
});
|
|
2316
|
+
explicitlyRequestedDiscoveredBuiltinToolNames = selectRestorableDiscoveredBuiltinToolNames(
|
|
2317
|
+
explicitRequestedToolNames,
|
|
2318
|
+
toolRegistry,
|
|
2319
|
+
allowedDiscoveredBuiltinNames,
|
|
2320
|
+
).filter(name => !essentialBuiltinNames.has(name));
|
|
2321
|
+
const requestedDiscoveredBuiltinToolNameSet = new Set(explicitlyRequestedDiscoveredBuiltinToolNames);
|
|
2322
|
+
initialBaselineDiscoveredBuiltinToolNames = selectRestorableDiscoveredBuiltinToolNames(
|
|
2323
|
+
baselineInitialToolNames.filter(name => !requestedDiscoveredBuiltinToolNameSet.has(name)),
|
|
2324
|
+
toolRegistry,
|
|
2325
|
+
allowedDiscoveredBuiltinNames,
|
|
2326
|
+
);
|
|
2327
|
+
const restoredDiscoveredNames = selectRestorableDiscoveredBuiltinToolNames(
|
|
2328
|
+
existingSession.selectedDiscoveredBuiltinToolNames ?? [],
|
|
2329
|
+
toolRegistry,
|
|
2330
|
+
allowedDiscoveredBuiltinNames,
|
|
2331
|
+
essentialBuiltinNames,
|
|
2332
|
+
);
|
|
2333
|
+
initialSelectedDiscoveredBuiltinToolNames = existingSession.hasPersistedDiscoveredBuiltinToolSelection
|
|
2334
|
+
? restoredDiscoveredNames
|
|
2335
|
+
: explicitlyRequestedDiscoveredBuiltinToolNames;
|
|
2336
|
+
initialToolNames = [...new Set([...baselineInitialToolNames, ...initialSelectedDiscoveredBuiltinToolNames])];
|
|
2337
|
+
hasExplicitDiscoveredBuiltinToolSelection =
|
|
2338
|
+
hasExplicitToolNames &&
|
|
2339
|
+
(options.toolNames!.length === 0 || explicitlyRequestedDiscoveredBuiltinToolNames.length > 0);
|
|
2224
2340
|
}
|
|
2225
2341
|
|
|
2226
2342
|
// Pre-register in the global agent registry BEFORE building the system prompt,
|
|
@@ -2456,7 +2572,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2456
2572
|
|
|
2457
2573
|
// Restore messages if session has existing data
|
|
2458
2574
|
if (hasExistingSession) {
|
|
2459
|
-
agent.replaceMessages(existingSession.messages);
|
|
2575
|
+
agent.replaceMessages(reconcileTrailingToolCalls(existingSession.messages));
|
|
2460
2576
|
} else {
|
|
2461
2577
|
// Save initial model, thinking level, and service tier for new sessions so they can be restored on resume.
|
|
2462
2578
|
if (model) {
|
|
@@ -2521,8 +2637,16 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2521
2637
|
mcpDiscoveryEnabled,
|
|
2522
2638
|
discoveryMode: effectiveDiscoveryMode,
|
|
2523
2639
|
initialSelectedMCPToolNames,
|
|
2640
|
+
initialMCPToolSelectionIsExplicit: hasExplicitMCPToolSelection,
|
|
2641
|
+
initialDiscoveredBuiltinToolSelectionIsExplicit: hasExplicitDiscoveredBuiltinToolSelection,
|
|
2642
|
+
initialSelectedDiscoveredBuiltinToolNames,
|
|
2643
|
+
initialBaselineDiscoveredBuiltinToolNames,
|
|
2524
2644
|
defaultSelectedMCPToolNames,
|
|
2525
|
-
|
|
2645
|
+
mandatoryMCPToolNames: pluginMcpToolNames,
|
|
2646
|
+
persistInitialMCPToolSelection: !hasExistingSession && hasExplicitMCPToolSelection,
|
|
2647
|
+
persistInitialDiscoveredBuiltinToolSelection: !hasExistingSession && hasExplicitDiscoveredBuiltinToolSelection,
|
|
2648
|
+
initialPersistedMCPToolNames: selectableExplicitMCPToolNames,
|
|
2649
|
+
initialPersistedDiscoveredBuiltinToolNames: explicitlyRequestedDiscoveredBuiltinToolNames,
|
|
2526
2650
|
defaultSelectedMCPServerNames: settings.get("mcp.discoveryDefaultServers") ?? [],
|
|
2527
2651
|
ttsrManager,
|
|
2528
2652
|
obfuscator,
|
|
@@ -2664,6 +2788,10 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2664
2788
|
});
|
|
2665
2789
|
}
|
|
2666
2790
|
|
|
2791
|
+
// Constructor-time workflow-gate tool restoration is deferred by one
|
|
2792
|
+
// microtask (the ToolSession closure needs `session` assigned). Await it
|
|
2793
|
+
// so a resumed canonical workflow session returns with `ask` resident.
|
|
2794
|
+
await session.workflowGateToolRestoration;
|
|
2667
2795
|
return {
|
|
2668
2796
|
session,
|
|
2669
2797
|
extensionsResult,
|
package/src/secrets/index.ts
CHANGED
|
@@ -1,31 +1,60 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import * as fs from "node:fs/promises";
|
|
1
3
|
import * as path from "node:path";
|
|
2
|
-
import { isEnoent, logger } from "@gajae-code/utils";
|
|
4
|
+
import { isEnoent, logger, pathIsWithin } from "@gajae-code/utils";
|
|
3
5
|
import { YAML } from "bun";
|
|
4
|
-
import type
|
|
6
|
+
import { type SecretEntry, SecretObfuscator } from "./obfuscator";
|
|
5
7
|
import { compileSecretRegex } from "./regex";
|
|
6
8
|
|
|
9
|
+
const PROCESS_SECRET_OBFUSCATION_KEY = randomBytes(32);
|
|
10
|
+
|
|
11
|
+
/** Create an obfuscator using the process-local authenticated-placeholder key. */
|
|
12
|
+
export function createSecretObfuscator(entries: SecretEntry[]): SecretObfuscator {
|
|
13
|
+
return new SecretObfuscator(entries, PROCESS_SECRET_OBFUSCATION_KEY);
|
|
14
|
+
}
|
|
7
15
|
export { deobfuscateSessionContext, obfuscateMessages, type SecretEntry, SecretObfuscator } from "./obfuscator";
|
|
8
16
|
|
|
17
|
+
type SecretsFileScope = "global" | "project";
|
|
18
|
+
|
|
9
19
|
/**
|
|
10
20
|
* Load secrets from project-local and global secrets.yml files.
|
|
11
|
-
* Project-local entries override global entries with matching content.
|
|
21
|
+
* Project-local plain entries override global entries with matching content.
|
|
12
22
|
*/
|
|
13
23
|
export async function loadSecrets(cwd: string, agentDir: string): Promise<SecretEntry[]> {
|
|
14
24
|
const projectPath = path.join(cwd, ".gjc", "secrets.yml");
|
|
15
25
|
const globalPath = path.join(agentDir, "secrets.yml");
|
|
26
|
+
const agentScope = await classifySecretsFileScope(cwd, agentDir);
|
|
16
27
|
|
|
17
|
-
const globalEntries = await loadSecretsFile(globalPath);
|
|
18
|
-
const projectEntries = await loadSecretsFile(projectPath);
|
|
28
|
+
const globalEntries = await loadSecretsFile(globalPath, agentScope);
|
|
29
|
+
const projectEntries = await loadSecretsFile(projectPath, "project");
|
|
19
30
|
|
|
20
31
|
if (globalEntries.length === 0) return projectEntries;
|
|
21
32
|
if (projectEntries.length === 0) return globalEntries;
|
|
22
33
|
|
|
23
|
-
//
|
|
24
|
-
const
|
|
25
|
-
const merged = [
|
|
34
|
+
// Project plain entries override matching global plain entries, never global regex entries.
|
|
35
|
+
const projectPlainContents = new Set(projectEntries.filter(e => e.type === "plain").map(e => e.content));
|
|
36
|
+
const merged = [
|
|
37
|
+
...globalEntries.filter(e => e.type === "regex" || !projectPlainContents.has(e.content)),
|
|
38
|
+
...projectEntries,
|
|
39
|
+
];
|
|
26
40
|
return merged;
|
|
27
41
|
}
|
|
28
42
|
|
|
43
|
+
function pathIsLexicallyWithin(root: string, candidate: string): boolean {
|
|
44
|
+
const relative = path.relative(path.resolve(root), path.resolve(candidate));
|
|
45
|
+
return relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function classifySecretsFileScope(cwd: string, agentDir: string): Promise<SecretsFileScope> {
|
|
49
|
+
if (pathIsLexicallyWithin(cwd, agentDir)) return "project";
|
|
50
|
+
try {
|
|
51
|
+
const [canonicalCwd, canonicalAgentDir] = await Promise.all([fs.realpath(cwd), fs.realpath(agentDir)]);
|
|
52
|
+
return pathIsWithin(canonicalCwd, canonicalAgentDir) ? "project" : "global";
|
|
53
|
+
} catch {
|
|
54
|
+
return "project";
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
29
58
|
/** Minimum env var value length to consider as a secret. */
|
|
30
59
|
const MIN_ENV_VALUE_LENGTH = 8;
|
|
31
60
|
|
|
@@ -46,7 +75,7 @@ export function collectEnvSecrets(): SecretEntry[] {
|
|
|
46
75
|
return entries;
|
|
47
76
|
}
|
|
48
77
|
|
|
49
|
-
async function loadSecretsFile(filePath: string): Promise<SecretEntry[]> {
|
|
78
|
+
async function loadSecretsFile(filePath: string, scope: SecretsFileScope): Promise<SecretEntry[]> {
|
|
50
79
|
try {
|
|
51
80
|
const text = await Bun.file(filePath).text();
|
|
52
81
|
const raw = YAML.parse(text);
|
|
@@ -55,8 +84,13 @@ async function loadSecretsFile(filePath: string): Promise<SecretEntry[]> {
|
|
|
55
84
|
return [];
|
|
56
85
|
}
|
|
57
86
|
const entries: SecretEntry[] = [];
|
|
87
|
+
let skippedProjectRegexEntries = 0;
|
|
58
88
|
for (let i = 0; i < raw.length; i++) {
|
|
59
89
|
const entry = raw[i];
|
|
90
|
+
if (scope === "project" && isRegexEntry(entry)) {
|
|
91
|
+
skippedProjectRegexEntries++;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
60
94
|
if (!validateEntry(entry, filePath, i)) continue;
|
|
61
95
|
entries.push({
|
|
62
96
|
type: entry.type,
|
|
@@ -66,6 +100,12 @@ async function loadSecretsFile(filePath: string): Promise<SecretEntry[]> {
|
|
|
66
100
|
flags: entry.flags,
|
|
67
101
|
});
|
|
68
102
|
}
|
|
103
|
+
if (skippedProjectRegexEntries > 0) {
|
|
104
|
+
logger.warn("Project secrets.yml regex entries are ignored; regex entries are global-only", {
|
|
105
|
+
path: filePath,
|
|
106
|
+
skippedEntries: skippedProjectRegexEntries,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
69
109
|
return entries;
|
|
70
110
|
} catch (err) {
|
|
71
111
|
if (isEnoent(err)) return [];
|
|
@@ -74,6 +114,10 @@ async function loadSecretsFile(filePath: string): Promise<SecretEntry[]> {
|
|
|
74
114
|
}
|
|
75
115
|
}
|
|
76
116
|
|
|
117
|
+
function isRegexEntry(entry: unknown): boolean {
|
|
118
|
+
return entry !== null && typeof entry === "object" && (entry as Record<string, unknown>).type === "regex";
|
|
119
|
+
}
|
|
120
|
+
|
|
77
121
|
function validateEntry(entry: unknown, filePath: string, index: number): entry is SecretEntry {
|
|
78
122
|
if (entry === null || typeof entry !== "object") {
|
|
79
123
|
logger.warn(`secrets.yml[${index}]: entry must be an object`, { path: filePath });
|
|
@@ -106,7 +150,6 @@ function validateEntry(entry: unknown, filePath: string, index: number): entry i
|
|
|
106
150
|
} catch (error) {
|
|
107
151
|
logger.warn(`secrets.yml[${index}]: invalid regex pattern`, {
|
|
108
152
|
path: filePath,
|
|
109
|
-
pattern: e.content,
|
|
110
153
|
error: String(error),
|
|
111
154
|
});
|
|
112
155
|
return false;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHmac, randomBytes } from "node:crypto";
|
|
1
2
|
import type { Message, TextContent } from "@gajae-code/ai";
|
|
2
3
|
import { type SessionContext, transferSessionMessageIdentity } from "../session/session-manager";
|
|
3
4
|
import { compileSecretRegex } from "./regex";
|
|
@@ -39,28 +40,28 @@ function generateDeterministicReplacement(secret: string): string {
|
|
|
39
40
|
// Placeholder format
|
|
40
41
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
41
42
|
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
/** Build
|
|
46
|
-
function buildPlaceholder(
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
return
|
|
43
|
+
const PLACEHOLDER_DOMAIN = "gjc.secret-obfuscation.placeholder.v1\0";
|
|
44
|
+
const PLACEHOLDER_RE = /#GJC1_[A-Za-z0-9_-]{22}#/g;
|
|
45
|
+
|
|
46
|
+
/** Build a versioned, authenticated placeholder whose identity depends only on the key and secret. */
|
|
47
|
+
function buildPlaceholder(secret: string, key: Uint8Array): string {
|
|
48
|
+
const tag = createHmac("sha256", key)
|
|
49
|
+
.update(PLACEHOLDER_DOMAIN)
|
|
50
|
+
.update(secret, "utf8")
|
|
51
|
+
.digest()
|
|
52
|
+
.subarray(0, 16)
|
|
53
|
+
.toString("base64url");
|
|
54
|
+
return `#GJC1_${tag}#`;
|
|
54
55
|
}
|
|
55
56
|
|
|
56
|
-
/** Regex to match obfuscation placeholders: #HASH# */
|
|
57
|
-
const PLACEHOLDER_RE = /#[A-Z0-9]{4}#/g;
|
|
58
|
-
|
|
59
57
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
60
58
|
// SecretObfuscator
|
|
61
59
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
62
60
|
|
|
63
61
|
export class SecretObfuscator {
|
|
62
|
+
/** Key used to authenticate reversible placeholders. */
|
|
63
|
+
#placeholderKey: Uint8Array;
|
|
64
|
+
|
|
64
65
|
/** Plain secrets: secret → index (known at construction) */
|
|
65
66
|
#plainMappings = new Map<string, number>();
|
|
66
67
|
|
|
@@ -97,14 +98,16 @@ export class SecretObfuscator {
|
|
|
97
98
|
/** Whether any secrets were configured */
|
|
98
99
|
#hasAny: boolean;
|
|
99
100
|
|
|
100
|
-
constructor(entries: SecretEntry[]) {
|
|
101
|
+
constructor(entries: SecretEntry[], key: Uint8Array = randomBytes(32)) {
|
|
102
|
+
if (key.byteLength !== 32) throw new Error("Secret obfuscation key must be 32 bytes");
|
|
103
|
+
this.#placeholderKey = Uint8Array.from(key);
|
|
101
104
|
let index = 0;
|
|
102
105
|
for (const entry of entries) {
|
|
103
106
|
const mode = entry.mode ?? "obfuscate";
|
|
104
107
|
|
|
105
108
|
if (entry.type === "plain") {
|
|
106
109
|
if (mode === "obfuscate") {
|
|
107
|
-
const placeholder = buildPlaceholder(
|
|
110
|
+
const placeholder = buildPlaceholder(entry.content, this.#placeholderKey);
|
|
108
111
|
this.#plainMappings.set(entry.content, index);
|
|
109
112
|
this.#obfuscateMappings.set(index, { secret: entry.content, placeholder });
|
|
110
113
|
this.#deobfuscateMap.set(placeholder, entry.content);
|
|
@@ -172,7 +175,7 @@ export class SecretObfuscator {
|
|
|
172
175
|
let index = this.#findObfuscateIndex(matchValue);
|
|
173
176
|
if (index === undefined) {
|
|
174
177
|
index = this.#nextIndex++;
|
|
175
|
-
const placeholder = buildPlaceholder(
|
|
178
|
+
const placeholder = buildPlaceholder(matchValue, this.#placeholderKey);
|
|
176
179
|
this.#obfuscateMappings.set(index, { secret: matchValue, placeholder });
|
|
177
180
|
this.#deobfuscateMap.set(placeholder, matchValue);
|
|
178
181
|
this.#obfuscateIndexBySecret.set(matchValue, index);
|
package/src/secrets/regex.ts
CHANGED
|
@@ -16,6 +16,8 @@ export function compileSecretRegex(pattern: string, flags?: string): RegExp {
|
|
|
16
16
|
const combined = new Set([...resolvedFlags, ...literalMatch[2]]);
|
|
17
17
|
resolvedFlags = [...combined].join("");
|
|
18
18
|
}
|
|
19
|
+
if (resolvedFlags.includes("y"))
|
|
20
|
+
throw new SyntaxError('The sticky "y" flag is incompatible with globally scanned secret regexes');
|
|
19
21
|
|
|
20
22
|
return new RegExp(resolvedPattern, enforceGlobalFlag(resolvedFlags));
|
|
21
23
|
}
|