@gajae-code/coding-agent 0.11.4 → 0.11.6
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 +23 -0
- package/dist/types/cli/notify-cli.d.ts +5 -1
- package/dist/types/config/settings-schema.d.ts +31 -1
- 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/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/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/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 +58 -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/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/bus/chat-daemon-cli.d.ts +3 -1
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +42 -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 +11 -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 +98 -18
- package/dist/types/session/session-storage.d.ts +27 -0
- package/dist/types/session/streaming-output.d.ts +4 -0
- package/dist/types/session/yield-queue.d.ts +8 -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 +48 -0
- package/dist/types/tools/bash-skill-urls.d.ts +2 -2
- package/dist/types/tools/index.d.ts +4 -0
- package/package.json +9 -9
- package/scripts/generate-sdk-operation-inventory.ts +3 -0
- package/src/cli/notify-cli.ts +40 -18
- package/src/commands/sdk.ts +7 -2
- package/src/config/settings-schema.ts +31 -1
- 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/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/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-runtime.ts +38 -12
- 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 +8 -7
- package/src/internal-urls/local-protocol.ts +416 -18
- package/src/lsp/index.ts +4 -88
- package/src/main.ts +36 -12
- package/src/modes/acp/acp-agent.ts +8 -0
- package/src/modes/components/ansi-display-validator.ts +54 -0
- package/src/modes/components/notifications-settings-editor.ts +26 -2
- package/src/modes/components/session-observer-overlay.ts +40 -24
- package/src/modes/components/settings-selector.ts +3 -1
- package/src/modes/components/tool-transcript-format.ts +321 -0
- package/src/modes/components/transcript-viewer-overlay.ts +39 -17
- package/src/modes/controllers/command-controller.ts +15 -0
- package/src/modes/controllers/extension-ui-controller.ts +14 -2
- package/src/modes/controllers/goal-mode-controller.ts +20 -1
- 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 +70 -8
- package/src/modes/interactive-mode.ts +1 -0
- package/src/modes/runtime-init.ts +16 -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/session-index.ts +122 -64
- package/src/sdk/bus/chat-daemon-cli.ts +72 -10
- package/src/sdk/bus/chat-daemon-control.ts +538 -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 +388 -94
- 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 +11 -3
- package/src/sdk/bus/telegram-daemon-control.ts +312 -76
- package/src/sdk/bus/telegram-daemon.ts +1916 -285
- package/src/sdk/host/control/dispatch.ts +14 -5
- package/src/sdk/protocol/operation-inventory.generated.json +29 -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 +1447 -852
- package/src/session/artifacts.ts +188 -23
- package/src/session/context-estimation.ts +7 -2
- package/src/session/internal/managed-session-scope.ts +200 -38
- package/src/session/internal/managed-session-storage.ts +1152 -46
- package/src/session/session-manager.ts +1121 -148
- package/src/session/session-storage.ts +109 -11
- package/src/session/streaming-output.ts +6 -0
- package/src/session/yield-queue.ts +20 -0
- package/src/slash-commands/builtin-registry.ts +36 -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-answer-registry.ts +15 -1
- package/src/tools/ask.ts +129 -32
- package/src/tools/bash-skill-urls.ts +14 -3
- package/src/tools/bash.ts +2 -2
- package/src/tools/index.ts +4 -0
- package/src/tools/plan-mode-guard.ts +1 -0
- package/src/tools/sqlite-reader.ts +3 -2
- package/src/tools/subagent.ts +2 -0
|
@@ -12,12 +12,11 @@ import * as path from "node:path";
|
|
|
12
12
|
|
|
13
13
|
import { isEnoent, logger, pathIsWithin } from "@gajae-code/utils";
|
|
14
14
|
|
|
15
|
-
import { cachePlugin } from "./cache";
|
|
15
|
+
import { cachePlugin, getCachedPluginPath } from "./cache";
|
|
16
16
|
import { classifySource, fetchMarketplace, parseMarketplaceCatalog, promoteCloneToCache } from "./fetcher";
|
|
17
17
|
import {
|
|
18
18
|
addInstalledPlugin,
|
|
19
19
|
addMarketplaceEntry,
|
|
20
|
-
collectReferencedPaths,
|
|
21
20
|
getInstalledPlugin,
|
|
22
21
|
getMarketplaceEntry,
|
|
23
22
|
readInstalledPluginsRegistry,
|
|
@@ -255,6 +254,7 @@ export class MarketplaceManager {
|
|
|
255
254
|
if (existing && existing.length > 0 && !force) {
|
|
256
255
|
throw new Error(`Plugin "${pluginId}" is already installed. Use force option to reinstall.`);
|
|
257
256
|
}
|
|
257
|
+
const existingCachePaths = existing ? await this.#validateCacheDeletionTargets(pluginId, existing) : [];
|
|
258
258
|
|
|
259
259
|
// 4. Resolve source path.
|
|
260
260
|
// marketplaceClonePath is the marketplace root — the directory containing .Anthropic model-plugin/
|
|
@@ -289,6 +289,9 @@ export class MarketplaceManager {
|
|
|
289
289
|
let cachePath!: string;
|
|
290
290
|
try {
|
|
291
291
|
version = await this.#resolvePluginVersion(pluginEntry, sourcePath);
|
|
292
|
+
if ([marketplace, name, version].some(component => /[. ]$/.test(component))) {
|
|
293
|
+
throw new Error(`Invalid cache identity for plugin "${pluginId}"`);
|
|
294
|
+
}
|
|
292
295
|
cachePath = await cachePlugin(sourcePath, this.#opts.pluginsCacheDir, marketplace, name, version);
|
|
293
296
|
await this.#writeEmbeddedLspConfig(pluginEntry, cachePath);
|
|
294
297
|
} finally {
|
|
@@ -311,11 +314,13 @@ export class MarketplaceManager {
|
|
|
311
314
|
? readInstalledPluginsRegistry(this.#opts.projectInstalledRegistryPath)
|
|
312
315
|
: Promise.resolve({ version: 2 as const, plugins: {} as Record<string, InstalledPluginEntry[]> }),
|
|
313
316
|
]);
|
|
314
|
-
const referenced =
|
|
317
|
+
const referenced = this.#collectReferencedCacheIdentities(userReg, projectReg);
|
|
318
|
+
const cachePathIdentity = this.#cacheDeletionIdentity(cachePath);
|
|
315
319
|
|
|
316
|
-
for (const
|
|
317
|
-
|
|
318
|
-
|
|
320
|
+
for (const existingCachePath of existingCachePaths) {
|
|
321
|
+
const existingIdentity = this.#cacheDeletionIdentity(existingCachePath);
|
|
322
|
+
if (existingIdentity !== cachePathIdentity && !referenced.has(existingIdentity)) {
|
|
323
|
+
await fs.rm(existingCachePath, { recursive: true, force: true });
|
|
319
324
|
}
|
|
320
325
|
}
|
|
321
326
|
}
|
|
@@ -395,6 +400,84 @@ export class MarketplaceManager {
|
|
|
395
400
|
return "0.0.0";
|
|
396
401
|
}
|
|
397
402
|
|
|
403
|
+
async #validateCacheDeletionTargets(pluginId: string, entries: readonly InstalledPluginEntry[]): Promise<string[]> {
|
|
404
|
+
const parsed = parsePluginId(pluginId);
|
|
405
|
+
if (!parsed) {
|
|
406
|
+
throw new Error(`Invalid plugin ID format: "${pluginId}". Expected "name@marketplace".`);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const targets: string[] = [];
|
|
410
|
+
for (const entry of entries) {
|
|
411
|
+
if ([parsed.marketplace, parsed.name, entry.version].some(component => /[. ]$/.test(component))) {
|
|
412
|
+
throw new Error(`Refusing to remove plugin "${pluginId}": invalid recorded cache identity`);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
let targetPath: string;
|
|
416
|
+
try {
|
|
417
|
+
targetPath = getCachedPluginPath(
|
|
418
|
+
this.#opts.pluginsCacheDir,
|
|
419
|
+
parsed.marketplace,
|
|
420
|
+
parsed.name,
|
|
421
|
+
entry.version,
|
|
422
|
+
);
|
|
423
|
+
} catch (error) {
|
|
424
|
+
throw new Error(`Refusing to remove plugin "${pluginId}": invalid recorded cache identity`, {
|
|
425
|
+
cause: error,
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (entry.installPath !== targetPath) {
|
|
430
|
+
throw new Error(`Refusing to remove plugin "${pluginId}": recorded install path is not its cache path`);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
try {
|
|
434
|
+
const stat = await fs.lstat(targetPath);
|
|
435
|
+
if (stat.isSymbolicLink()) {
|
|
436
|
+
throw new Error(`Refusing to remove plugin "${pluginId}": cache path is a symbolic link or junction`);
|
|
437
|
+
}
|
|
438
|
+
} catch (error) {
|
|
439
|
+
if (!isEnoent(error)) throw error;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
targets.push(targetPath);
|
|
443
|
+
}
|
|
444
|
+
return targets;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
#cacheDeletionIdentity(cachePath: string): string {
|
|
448
|
+
// Model Windows path identity on every host so reference protection is
|
|
449
|
+
// deterministic: case is ignored and trailing dots/spaces are discarded.
|
|
450
|
+
return path
|
|
451
|
+
.resolve(cachePath)
|
|
452
|
+
.split(path.sep)
|
|
453
|
+
.map(component => component.replace(/[. ]+$/, "").toLowerCase())
|
|
454
|
+
.join(path.sep);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
#collectReferencedCacheIdentities(...registries: InstalledPluginsRegistry[]): Set<string> {
|
|
458
|
+
const identities = new Set<string>();
|
|
459
|
+
for (const registry of registries) {
|
|
460
|
+
for (const [pluginId, entries] of Object.entries(registry.plugins)) {
|
|
461
|
+
const parsed = parsePluginId(pluginId);
|
|
462
|
+
if (!parsed) continue;
|
|
463
|
+
for (const entry of entries) {
|
|
464
|
+
try {
|
|
465
|
+
const cachePath = getCachedPluginPath(
|
|
466
|
+
this.#opts.pluginsCacheDir,
|
|
467
|
+
parsed.marketplace,
|
|
468
|
+
parsed.name,
|
|
469
|
+
entry.version,
|
|
470
|
+
);
|
|
471
|
+
identities.add(this.#cacheDeletionIdentity(cachePath));
|
|
472
|
+
} catch {
|
|
473
|
+
// Invalid registry entries cannot identify a permitted cache target.
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
return identities;
|
|
479
|
+
}
|
|
480
|
+
|
|
398
481
|
async uninstallPlugin(pluginId: string, scope?: "user" | "project"): Promise<void> {
|
|
399
482
|
const parsed = parsePluginId(pluginId);
|
|
400
483
|
if (!parsed) {
|
|
@@ -434,6 +517,7 @@ export class MarketplaceManager {
|
|
|
434
517
|
const targetEntries = targetScope === "project" ? projectEntries! : userEntries!;
|
|
435
518
|
const targetReg = targetScope === "project" ? projectReg : userReg;
|
|
436
519
|
const registryPath = this.#registryPath(targetScope);
|
|
520
|
+
const targetCachePaths = await this.#validateCacheDeletionTargets(pluginId, targetEntries);
|
|
437
521
|
|
|
438
522
|
const updatedReg = removeInstalledPlugin(targetReg, pluginId);
|
|
439
523
|
await writeInstalledPluginsRegistry(registryPath, updatedReg);
|
|
@@ -445,11 +529,11 @@ export class MarketplaceManager {
|
|
|
445
529
|
? readInstalledPluginsRegistry(this.#opts.projectInstalledRegistryPath)
|
|
446
530
|
: Promise.resolve({ version: 2 as const, plugins: {} as Record<string, InstalledPluginEntry[]> }),
|
|
447
531
|
]);
|
|
448
|
-
const referenced =
|
|
532
|
+
const referenced = this.#collectReferencedCacheIdentities(freshUserReg, freshProjectReg);
|
|
449
533
|
|
|
450
|
-
for (const
|
|
451
|
-
if (!referenced.has(
|
|
452
|
-
await fs.rm(
|
|
534
|
+
for (const targetCachePath of targetCachePaths) {
|
|
535
|
+
if (!referenced.has(this.#cacheDeletionIdentity(targetCachePath))) {
|
|
536
|
+
await fs.rm(targetCachePath, { recursive: true, force: true });
|
|
453
537
|
}
|
|
454
538
|
}
|
|
455
539
|
|
|
@@ -8,7 +8,9 @@ import {
|
|
|
8
8
|
} from "./deep-interview-ambiguity";
|
|
9
9
|
import {
|
|
10
10
|
answerHash,
|
|
11
|
+
assertDeepInterviewInputWithinLimit,
|
|
11
12
|
assertDeepInterviewIntentManifest,
|
|
13
|
+
assertDeepInterviewStructuredResponseWithinLimit,
|
|
12
14
|
createDeepInterviewIntentManifest,
|
|
13
15
|
type DeepInterviewEstablishedFact,
|
|
14
16
|
type DeepInterviewIntentItem,
|
|
@@ -17,6 +19,7 @@ import {
|
|
|
17
19
|
type DeepInterviewStateEnvelope,
|
|
18
20
|
type DeepInterviewTriggerMetadata,
|
|
19
21
|
deriveRoundKey,
|
|
22
|
+
MAX_USER_RESPONSE_LENGTH,
|
|
20
23
|
normalizeDeepInterviewEnvelope,
|
|
21
24
|
questionHash,
|
|
22
25
|
reviewDeepInterviewIntent,
|
|
@@ -426,6 +429,9 @@ export async function appendOrMergeDeepInterviewRound(
|
|
|
426
429
|
input: DeepInterviewAnswerInput,
|
|
427
430
|
options: { sessionId?: string } = {},
|
|
428
431
|
): Promise<{ action: AppendOrMergeAction; record: DeepInterviewRoundRecord; disputedFactIds?: string[] }> {
|
|
432
|
+
assertDeepInterviewStructuredResponseWithinLimit(input);
|
|
433
|
+
if (input.customInput !== undefined)
|
|
434
|
+
assertDeepInterviewInputWithinLimit(input.customInput, MAX_USER_RESPONSE_LENGTH, "user_response");
|
|
429
435
|
const envelope = await readEnvelope(statePath);
|
|
430
436
|
const interviewId = input.interviewId ?? interviewIdOf(envelope);
|
|
431
437
|
const shell = buildAnswerShell({
|
|
@@ -547,6 +553,7 @@ export async function enrichDeepInterviewRoundScoring(
|
|
|
547
553
|
input: DeepInterviewScoringInput,
|
|
548
554
|
options: { sessionId?: string } = {},
|
|
549
555
|
): Promise<{ record: DeepInterviewRoundRecord }> {
|
|
556
|
+
assertDeepInterviewStructuredResponseWithinLimit(input);
|
|
550
557
|
const envelope = await readEnvelope(statePath);
|
|
551
558
|
const interviewId = input.interviewId ?? interviewIdOf(envelope);
|
|
552
559
|
const rounds = readRounds(envelope);
|
|
@@ -7,11 +7,14 @@ import { syncSkillActiveState } from "../skill-state/active-state";
|
|
|
7
7
|
import { deriveDeepInterviewHud } from "../skill-state/workflow-hud";
|
|
8
8
|
import { WORKFLOW_STATE_VERSION } from "../skill-state/workflow-state-contract";
|
|
9
9
|
import {
|
|
10
|
+
assertDeepInterviewInputWithinLimit,
|
|
10
11
|
assertDeepInterviewIntentReview,
|
|
11
12
|
type DeepInterviewIntentCategory,
|
|
12
13
|
type DeepInterviewIntentItem,
|
|
13
14
|
type DeepInterviewIntentManifest,
|
|
14
15
|
type DeepInterviewIntentReview,
|
|
16
|
+
MAX_DEEP_INTERVIEW_STRUCTURED_RESPONSE_LENGTH,
|
|
17
|
+
MAX_INITIAL_CONTEXT_LENGTH,
|
|
15
18
|
normalizeDeepInterviewEnvelope,
|
|
16
19
|
reviewDeepInterviewIntent,
|
|
17
20
|
} from "./deep-interview-state";
|
|
@@ -559,6 +562,7 @@ async function resolveDeepInterviewArgs(args: readonly string[], cwd: string): P
|
|
|
559
562
|
ideaParts.push(arg);
|
|
560
563
|
}
|
|
561
564
|
const idea = ideaParts.join(" ").trim();
|
|
565
|
+
assertDeepInterviewInputWithinLimit(idea, MAX_INITIAL_CONTEXT_LENGTH, "initial_idea");
|
|
562
566
|
const effectiveResolution: DeepInterviewResolution = resolution ?? "standard";
|
|
563
567
|
const trace = hasFlag(args, "--trace") && idea ? await buildDeepInterviewTraceSummary(cwd, idea) : undefined;
|
|
564
568
|
return {
|
|
@@ -632,6 +636,11 @@ export async function persistDeepInterviewSpec(
|
|
|
632
636
|
cwd: string,
|
|
633
637
|
resolved: ResolvedDeepInterviewSpecWriteArgs,
|
|
634
638
|
): Promise<PersistedDeepInterviewSpec> {
|
|
639
|
+
assertDeepInterviewInputWithinLimit(
|
|
640
|
+
resolved.spec,
|
|
641
|
+
MAX_DEEP_INTERVIEW_STRUCTURED_RESPONSE_LENGTH,
|
|
642
|
+
"structured deep-interview response",
|
|
643
|
+
);
|
|
635
644
|
const statePath = deepInterviewStatePath(cwd, resolved.sessionId);
|
|
636
645
|
const existingRead = await readExistingStateForMutation(statePath);
|
|
637
646
|
if (existingRead.kind === "corrupt" && !resolved.force) {
|
|
@@ -731,6 +740,7 @@ export async function persistDeepInterviewSpec(
|
|
|
731
740
|
|
|
732
741
|
async function seedDeepInterviewState(cwd: string, resolved: ResolvedDeepInterviewArgs): Promise<string> {
|
|
733
742
|
const statePath = deepInterviewStatePath(cwd, resolved.sessionId);
|
|
743
|
+
assertDeepInterviewInputWithinLimit(resolved.idea, MAX_INITIAL_CONTEXT_LENGTH, "initial_idea");
|
|
734
744
|
const now = new Date().toISOString();
|
|
735
745
|
const payload: Record<string, unknown> = {
|
|
736
746
|
active: true,
|
|
@@ -397,6 +397,126 @@ const MAX_INTENT_STATEMENT_LENGTH = 1_000;
|
|
|
397
397
|
const MAX_INTENT_RATIONALE_LENGTH = 500;
|
|
398
398
|
const MAX_INTENT_EVIDENCE_LENGTH = 80;
|
|
399
399
|
|
|
400
|
+
// =============================================================================
|
|
401
|
+
// Input validation: free-text field allowlist + size limits (ouroboros parity)
|
|
402
|
+
// =============================================================================
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* User-input fields that legitimately carry prose (goals, prompts, descriptions,
|
|
406
|
+
* answers). Shell metacharacters (`;`, `|`, `&`, backticks, `$()`) are valid prose
|
|
407
|
+
* here and must NOT be rejected as structural injection. Structural fields (ids,
|
|
408
|
+
* categories, hashes) stay strictly validated by their own guards.
|
|
409
|
+
*/
|
|
410
|
+
export const DEEP_INTERVIEW_FREETEXT_FIELDS: ReadonlySet<string> = new Set([
|
|
411
|
+
"initial_context",
|
|
412
|
+
"initial_idea",
|
|
413
|
+
"initial_context_summary",
|
|
414
|
+
"user_response",
|
|
415
|
+
"answer",
|
|
416
|
+
"goal",
|
|
417
|
+
"objective",
|
|
418
|
+
"prompt",
|
|
419
|
+
"description",
|
|
420
|
+
"statement",
|
|
421
|
+
"restated_goal",
|
|
422
|
+
"evidence",
|
|
423
|
+
"excerpt",
|
|
424
|
+
]);
|
|
425
|
+
|
|
426
|
+
/** DoS-prevention character-count caps, matching ask-schema string-length semantics. */
|
|
427
|
+
export const MAX_INITIAL_CONTEXT_LENGTH = 50_000;
|
|
428
|
+
export const MAX_USER_RESPONSE_LENGTH = 10_000;
|
|
429
|
+
/** Maximum serialized JavaScript-string length for one LLM-produced structured response. */
|
|
430
|
+
export const MAX_DEEP_INTERVIEW_STRUCTURED_RESPONSE_LENGTH = 100_000;
|
|
431
|
+
|
|
432
|
+
/** Count Unicode code points without allocating an intermediate character array. */
|
|
433
|
+
export function deepInterviewCharacterCount(value: string): number {
|
|
434
|
+
let count = 0;
|
|
435
|
+
for (const _character of value) count++;
|
|
436
|
+
return count;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
export function isDeepInterviewFreeTextField(name: string): boolean {
|
|
440
|
+
return DEEP_INTERVIEW_FREETEXT_FIELDS.has(name);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Assert that one structured deep-interview response is JSON-serializable and
|
|
445
|
+
* bounded by JavaScript string length. This deliberately does not measure or
|
|
446
|
+
* cap accumulated persisted interview state.
|
|
447
|
+
*/
|
|
448
|
+
export function assertDeepInterviewStructuredResponseWithinLimit(value: unknown): void {
|
|
449
|
+
let serialized: string | undefined;
|
|
450
|
+
try {
|
|
451
|
+
serialized = JSON.stringify(value, (_key, nestedValue: unknown) => {
|
|
452
|
+
if (
|
|
453
|
+
typeof nestedValue === "bigint" ||
|
|
454
|
+
typeof nestedValue === "function" ||
|
|
455
|
+
typeof nestedValue === "symbol" ||
|
|
456
|
+
(typeof nestedValue === "number" && !Number.isFinite(nestedValue))
|
|
457
|
+
)
|
|
458
|
+
throw new Error("invalid structured deep-interview response");
|
|
459
|
+
return nestedValue;
|
|
460
|
+
});
|
|
461
|
+
} catch {
|
|
462
|
+
throw new Error("invalid structured deep-interview response");
|
|
463
|
+
}
|
|
464
|
+
if (typeof serialized !== "string") throw new Error("invalid structured deep-interview response");
|
|
465
|
+
if (deepInterviewCharacterCount(serialized) > MAX_DEEP_INTERVIEW_STRUCTURED_RESPONSE_LENGTH)
|
|
466
|
+
throw new Error(
|
|
467
|
+
`structured deep-interview response exceeds max length ${MAX_DEEP_INTERVIEW_STRUCTURED_RESPONSE_LENGTH}`,
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Assert a free-text input is within its size cap. Never inspects content for shell
|
|
473
|
+
* metacharacters — free-text fields accept prose verbatim; this only bounds length.
|
|
474
|
+
*/
|
|
475
|
+
export function assertDeepInterviewInputWithinLimit(value: string, max: number, fieldName = "input"): void {
|
|
476
|
+
if (typeof value !== "string") throw new Error(`${fieldName} must be a string`);
|
|
477
|
+
if (deepInterviewCharacterCount(value) > max) throw new Error(`${fieldName} exceeds max length ${max}`);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/** Validate user-supplied deep-interview prose before an envelope is persisted. */
|
|
481
|
+
export function assertDeepInterviewEnvelopeInputLimits(envelope: Record<string, unknown>): void {
|
|
482
|
+
const state =
|
|
483
|
+
typeof envelope.state === "object" && envelope.state !== null && !Array.isArray(envelope.state)
|
|
484
|
+
? (envelope.state as Record<string, unknown>)
|
|
485
|
+
: {};
|
|
486
|
+
for (const field of ["initial_idea", "initial_context", "initial_context_summary"] as const) {
|
|
487
|
+
const nestedValue = state[field];
|
|
488
|
+
if (nestedValue !== undefined)
|
|
489
|
+
assertDeepInterviewInputWithinLimit(nestedValue as string, MAX_INITIAL_CONTEXT_LENGTH, `state.${field}`);
|
|
490
|
+
const topLevelValue = envelope[field];
|
|
491
|
+
if (topLevelValue !== undefined)
|
|
492
|
+
assertDeepInterviewInputWithinLimit(topLevelValue as string, MAX_INITIAL_CONTEXT_LENGTH, field);
|
|
493
|
+
}
|
|
494
|
+
for (const field of ["user_response", "answer"] as const) {
|
|
495
|
+
const nestedValue = state[field];
|
|
496
|
+
if (nestedValue !== undefined)
|
|
497
|
+
assertDeepInterviewInputWithinLimit(nestedValue as string, MAX_USER_RESPONSE_LENGTH, `state.${field}`);
|
|
498
|
+
const topLevelValue = envelope[field];
|
|
499
|
+
if (topLevelValue !== undefined)
|
|
500
|
+
assertDeepInterviewInputWithinLimit(topLevelValue as string, MAX_USER_RESPONSE_LENGTH, field);
|
|
501
|
+
}
|
|
502
|
+
if (!Array.isArray(state.rounds)) return;
|
|
503
|
+
for (const [index, round] of state.rounds.entries()) {
|
|
504
|
+
if (typeof round !== "object" || round === null || Array.isArray(round)) continue;
|
|
505
|
+
const record = round as Record<string, unknown>;
|
|
506
|
+
for (const field of ["custom_input", "customInput", "user_response", "answer"] as const) {
|
|
507
|
+
const value = record[field];
|
|
508
|
+
// A structured scorer may use an unrelated object-valued `answer`; only prose
|
|
509
|
+
// values in a legacy answer slot are user input subject to this cap.
|
|
510
|
+
if (value === undefined || (field === "answer" && typeof value !== "string")) continue;
|
|
511
|
+
assertDeepInterviewInputWithinLimit(
|
|
512
|
+
value as string,
|
|
513
|
+
MAX_USER_RESPONSE_LENGTH,
|
|
514
|
+
`state.rounds[${index}].${field}`,
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
400
520
|
export interface DeepInterviewIntentItem {
|
|
401
521
|
id: string;
|
|
402
522
|
category: DeepInterviewIntentCategory;
|
|
@@ -437,7 +557,7 @@ export function isCanonicalDeepInterviewAnswerHash(value: unknown): value is str
|
|
|
437
557
|
function isEvidenceReference(value: unknown, answerHashValue: string): value is string {
|
|
438
558
|
return (
|
|
439
559
|
typeof value === "string" &&
|
|
440
|
-
value
|
|
560
|
+
deepInterviewCharacterCount(value) <= MAX_INTENT_EVIDENCE_LENGTH &&
|
|
441
561
|
value === `answer_hash:${answerHashValue}`
|
|
442
562
|
);
|
|
443
563
|
}
|
|
@@ -451,7 +571,11 @@ function assertIntentItem(value: unknown): asserts value is DeepInterviewIntentI
|
|
|
451
571
|
!id.startsWith(`${category}:`)
|
|
452
572
|
)
|
|
453
573
|
throw new Error("invalid intent category");
|
|
454
|
-
if (
|
|
574
|
+
if (
|
|
575
|
+
typeof statement !== "string" ||
|
|
576
|
+
!statement.trim() ||
|
|
577
|
+
deepInterviewCharacterCount(statement) > MAX_INTENT_STATEMENT_LENGTH
|
|
578
|
+
)
|
|
455
579
|
throw new Error(`intent item ${id} requires a bounded statement`);
|
|
456
580
|
}
|
|
457
581
|
|
|
@@ -541,7 +665,7 @@ export function reviewDeepInterviewIntent(
|
|
|
541
665
|
substitution.replacement_ids.some(id => typeof id !== "string" || !observedIds.has(id)) ||
|
|
542
666
|
typeof substitution.rationale !== "string" ||
|
|
543
667
|
!substitution.rationale.trim() ||
|
|
544
|
-
substitution.rationale
|
|
668
|
+
deepInterviewCharacterCount(substitution.rationale) > MAX_INTENT_RATIONALE_LENGTH
|
|
545
669
|
)
|
|
546
670
|
throw new Error("invalid intent substitution");
|
|
547
671
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { Buffer } from "node:buffer";
|
|
2
1
|
import * as crypto from "node:crypto";
|
|
3
2
|
import * as fs from "node:fs";
|
|
4
3
|
import * as path from "node:path";
|
|
@@ -6,6 +5,18 @@ import { VERSION } from "@gajae-code/utils/dirs";
|
|
|
6
5
|
import { safeStderrWrite } from "@gajae-code/utils/safe-stderr";
|
|
7
6
|
import type { Args } from "../cli/args";
|
|
8
7
|
import { readLinuxProcStartTimeSync } from "./linux-proc";
|
|
8
|
+
import {
|
|
9
|
+
MANAGED_OWNER_PREDECESSOR_GENERATION_ENV,
|
|
10
|
+
MANAGED_OWNER_PREDECESSOR_INCARNATION_ENV,
|
|
11
|
+
MANAGED_OWNER_PREDECESSOR_RUN_ID_ENV,
|
|
12
|
+
MANAGED_OWNER_PREDECESSOR_TOKEN_ENV,
|
|
13
|
+
MANAGED_OWNER_TRANSCRIPT_PATH_ENV,
|
|
14
|
+
} from "./managed-owner-admission";
|
|
15
|
+
import {
|
|
16
|
+
MANAGED_OWNER_INCARNATION_ENV,
|
|
17
|
+
MANAGED_OWNER_RUN_ID_ENV,
|
|
18
|
+
MANAGED_OWNER_SUPERVISOR_ARG,
|
|
19
|
+
} from "./managed-owner-supervisor";
|
|
9
20
|
import { tmuxRuntimeSessionPath } from "./session-layout";
|
|
10
21
|
import {
|
|
11
22
|
GJC_COORDINATOR_SESSION_BRANCH_ENV,
|
|
@@ -42,6 +53,7 @@ import {
|
|
|
42
53
|
type OwnerIsolationProbeSync,
|
|
43
54
|
planTmuxOwnerIsolationSync,
|
|
44
55
|
replaceOwnerGenerationSync,
|
|
56
|
+
resolveManagedOwnerPredecessorSync,
|
|
45
57
|
type TmuxServerProof,
|
|
46
58
|
} from "./tmux-owner-isolation";
|
|
47
59
|
import {
|
|
@@ -152,6 +164,9 @@ export interface TmuxLaunchPlan {
|
|
|
152
164
|
sessionStateFile?: string | null;
|
|
153
165
|
/** Immutable Linux-managed owner provenance, assigned immediately before creation. */
|
|
154
166
|
ownerGeneration?: string;
|
|
167
|
+
/** Immutable run and endpoint identities bound into the supervised command. */
|
|
168
|
+
ownerRunId?: string;
|
|
169
|
+
ownerIncarnation?: string;
|
|
155
170
|
/** Generation state captured before owner-isolation planning; required for publication CAS. */
|
|
156
171
|
ownerGenerationBaseline?: import("./tmux-owner-isolation").OwnerGenerationBaseline;
|
|
157
172
|
/** Native tmux session identity emitted atomically by `new-session -P -F`. */
|
|
@@ -334,6 +349,7 @@ interface CommandResolutionContext {
|
|
|
334
349
|
extraEnv?: Record<string, string>;
|
|
335
350
|
tmuxExitMarkerPath?: string;
|
|
336
351
|
platform?: NodeJS.Platform;
|
|
352
|
+
managedOwnerSupervisor?: boolean;
|
|
337
353
|
}
|
|
338
354
|
|
|
339
355
|
function parseLaunchPolicy(env: NodeJS.ProcessEnv): LaunchPolicy {
|
|
@@ -553,8 +569,15 @@ function buildInnerCommand(context: CommandResolutionContext, rawArgs: string[])
|
|
|
553
569
|
tmuxExitMarkerPath: context.tmuxExitMarkerPath,
|
|
554
570
|
});
|
|
555
571
|
const command = resolveCurrentGjcCommand(context);
|
|
556
|
-
const
|
|
557
|
-
const
|
|
572
|
+
const childArgs = stripRootTmuxFlag(rawArgs);
|
|
573
|
+
const supervisorEnv: Record<string, string> = context.managedOwnerSupervisor
|
|
574
|
+
? { GJC_MANAGED_OWNER_COMMAND_JSON: JSON.stringify([...command, ...childArgs]) }
|
|
575
|
+
: {};
|
|
576
|
+
const invocationArgs = context.managedOwnerSupervisor
|
|
577
|
+
? [...command, MANAGED_OWNER_SUPERVISOR_ARG]
|
|
578
|
+
: [...command, ...childArgs];
|
|
579
|
+
const quoted = invocationArgs.map(shellQuote).join(" ");
|
|
580
|
+
const invocation = `env ${GJC_TMUX_LAUNCHED_ENV}=1${buildEnvAssignments({ ...context.extraEnv, ...supervisorEnv })} ${quoted}`;
|
|
558
581
|
if (!context.tmuxExitMarkerPath) return `exec ${invocation}`;
|
|
559
582
|
return `${buildPosixTmuxExitMarkerPrefix(context.tmuxExitMarkerPath)}; ${invocation}; exit $?`;
|
|
560
583
|
}
|
|
@@ -1026,15 +1049,29 @@ export function buildDefaultTmuxLaunchPlan(context: TmuxLaunchContext): TmuxLaun
|
|
|
1026
1049
|
};
|
|
1027
1050
|
}
|
|
1028
1051
|
|
|
1052
|
+
function trustedReplacementAuthority(
|
|
1053
|
+
stateDir: string,
|
|
1054
|
+
sessionId: string,
|
|
1055
|
+
baseline: ReturnType<typeof captureOwnerGenerationBaselineSync>,
|
|
1056
|
+
): ReturnType<typeof resolveManagedOwnerPredecessorSync> {
|
|
1057
|
+
return resolveManagedOwnerPredecessorSync(stateDir, sessionId, baseline);
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1029
1060
|
function prepareManagedOwnerLifecycle(plan: TmuxLaunchPlan, context: TmuxLaunchContext): void {
|
|
1030
1061
|
if (plan.ownerGeneration) return;
|
|
1031
1062
|
const sessionId = plan.sessionId ?? plan.sessionName;
|
|
1032
1063
|
const stateDir = path.dirname(plan.sessionStateFile ?? path.join(plan.cwd, ".gjc", "runtime"));
|
|
1064
|
+
const baseline = captureOwnerGenerationBaselineSync(stateDir, sessionId);
|
|
1065
|
+
const replacement = trustedReplacementAuthority(stateDir, sessionId, baseline);
|
|
1033
1066
|
const generation = crypto.randomUUID();
|
|
1034
|
-
|
|
1067
|
+
const runId = crypto.randomUUID();
|
|
1068
|
+
const incarnation = crypto.randomUUID();
|
|
1069
|
+
plan.ownerGenerationBaseline = baseline;
|
|
1070
|
+
// Stage immutable identity in the child command. It becomes current only after
|
|
1035
1071
|
// immutable creation proof and ownership tagging complete.
|
|
1036
|
-
|
|
1037
1072
|
plan.ownerGeneration = generation;
|
|
1073
|
+
plan.ownerRunId = runId;
|
|
1074
|
+
plan.ownerIncarnation = incarnation;
|
|
1038
1075
|
const innerCommand = buildInnerCommand(
|
|
1039
1076
|
{
|
|
1040
1077
|
cwd: plan.cwd,
|
|
@@ -1047,11 +1084,24 @@ function prepareManagedOwnerLifecycle(plan: TmuxLaunchPlan, context: TmuxLaunchC
|
|
|
1047
1084
|
[GJC_TMUX_OWNER_GENERATION_ENV]: generation,
|
|
1048
1085
|
[GJC_TMUX_OWNER_STATE_DIR_ENV]: stateDir,
|
|
1049
1086
|
[GJC_TMUX_OWNER_SERVER_KEY_ENV]: plan.tmuxCommand,
|
|
1087
|
+
[MANAGED_OWNER_RUN_ID_ENV]: runId,
|
|
1088
|
+
[MANAGED_OWNER_INCARNATION_ENV]: incarnation,
|
|
1089
|
+
...(replacement
|
|
1090
|
+
? {
|
|
1091
|
+
[MANAGED_OWNER_PREDECESSOR_TOKEN_ENV]: replacement.predecessorToken,
|
|
1092
|
+
[MANAGED_OWNER_PREDECESSOR_GENERATION_ENV]: replacement.generation,
|
|
1093
|
+
[MANAGED_OWNER_PREDECESSOR_RUN_ID_ENV]: replacement.runId,
|
|
1094
|
+
[MANAGED_OWNER_PREDECESSOR_INCARNATION_ENV]: replacement.incarnation,
|
|
1095
|
+
[MANAGED_OWNER_TRANSCRIPT_PATH_ENV]:
|
|
1096
|
+
context.env?.GJC_SESSION_FILE ?? process.env.GJC_SESSION_FILE ?? "",
|
|
1097
|
+
}
|
|
1098
|
+
: {}),
|
|
1050
1099
|
},
|
|
1051
1100
|
// Linux managed owner close signals the pane PID. Do not place the exit-marker shell
|
|
1052
1101
|
// in front of it; `buildInnerCommand` therefore execs the GJC owner directly.
|
|
1053
1102
|
tmuxExitMarkerPath: plan.platform === "linux" ? undefined : tmuxExitMarkerPath(plan.sessionStateFile ?? ""),
|
|
1054
1103
|
platform: plan.platform,
|
|
1104
|
+
managedOwnerSupervisor: plan.platform === "linux",
|
|
1055
1105
|
},
|
|
1056
1106
|
context.rawArgs,
|
|
1057
1107
|
);
|
|
@@ -1182,7 +1232,7 @@ function createIsolatedTmuxSession(
|
|
|
1182
1232
|
): TmuxSpawnResult {
|
|
1183
1233
|
const sessionId = plan.sessionId ?? plan.sessionName;
|
|
1184
1234
|
const stateDir = path.dirname(plan.sessionStateFile ?? path.join(plan.cwd, ".gjc", "runtime"));
|
|
1185
|
-
const baseline = captureOwnerGenerationBaselineSync(stateDir, sessionId);
|
|
1235
|
+
const baseline = plan.ownerGenerationBaseline ?? captureOwnerGenerationBaselineSync(stateDir, sessionId);
|
|
1186
1236
|
plan.ownerGenerationBaseline = baseline;
|
|
1187
1237
|
const ownerPlan = planTmuxOwnerIsolationSync(
|
|
1188
1238
|
{
|
|
@@ -1572,6 +1622,11 @@ export function launchDefaultTmuxIfNeeded(context: TmuxLaunchContext): boolean {
|
|
|
1572
1622
|
return true;
|
|
1573
1623
|
}
|
|
1574
1624
|
try {
|
|
1625
|
+
resolveManagedOwnerPredecessorSync(
|
|
1626
|
+
path.dirname(plan.sessionStateFile!),
|
|
1627
|
+
plan.sessionId!,
|
|
1628
|
+
plan.ownerGenerationBaseline!,
|
|
1629
|
+
);
|
|
1575
1630
|
replaceOwnerGenerationSync(
|
|
1576
1631
|
path.dirname(plan.sessionStateFile!),
|
|
1577
1632
|
plan.sessionId!,
|