@webpresso/agent-kit 3.3.2 → 3.3.4
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/THIRD-PARTY-NOTICES.md +12 -10
- package/catalog/AGENTS.md.tpl +3 -5
- package/catalog/agent/rules/changeset-release.md +15 -4
- package/catalog/agent/rules/managed-tool-binaries.md +67 -0
- package/catalog/agent/rules/parallel-orchestration-quota.md +99 -1
- package/catalog/agent/rules/pre-implementation.md +36 -20
- package/catalog/agent/rules/rtk-routing.md +22 -16
- package/catalog/agent/skills/autopilot/SKILL.md +4 -3
- package/catalog/agent/skills/ultragoal/SKILL.md +11 -9
- package/catalog/agent/skills/verify/SKILL.md +12 -12
- package/catalog/compose/admin-blocks-registry.json +659 -0
- package/dist/esm/audit/hook-surface.js +5 -1
- package/dist/esm/audit/repo-guardrails.d.ts +8 -0
- package/dist/esm/audit/repo-guardrails.js +71 -1
- package/dist/esm/blueprint/core/parser.js +40 -16
- package/dist/esm/blueprint/core/validation/criteria.js +2 -1
- package/dist/esm/blueprint/execution/artifacts.js +8 -4
- package/dist/esm/blueprint/execution/frontmatter-keys.d.ts +17 -0
- package/dist/esm/blueprint/execution/frontmatter-keys.js +24 -0
- package/dist/esm/blueprint/execution/metadata.js +8 -4
- package/dist/esm/blueprint/lifecycle/audit.d.ts +21 -0
- package/dist/esm/blueprint/lifecycle/audit.js +89 -3
- package/dist/esm/blueprint/lifecycle/review-provenance.js +148 -40
- package/dist/esm/blueprint/lifecycle/rollout-anchor.d.ts +22 -0
- package/dist/esm/blueprint/lifecycle/rollout-anchor.js +37 -1
- package/dist/esm/blueprint/markdown/blocked-line.d.ts +49 -0
- package/dist/esm/blueprint/markdown/blocked-line.js +63 -0
- package/dist/esm/blueprint/markdown/helpers.js +10 -4
- package/dist/esm/blueprint/tracked-document/parser.js +3 -3
- package/dist/esm/blueprint/trust/gate-row.d.ts +55 -0
- package/dist/esm/blueprint/trust/gate-row.js +113 -0
- package/dist/esm/blueprint/trust/promotion.d.ts +17 -0
- package/dist/esm/blueprint/trust/promotion.js +19 -19
- package/dist/esm/build/atomic-file-copy.d.ts +32 -0
- package/dist/esm/build/atomic-file-copy.js +44 -0
- package/dist/esm/build/cli-mcp-parity.js +3 -2
- package/dist/esm/build/package-manifest.js +7 -2
- package/dist/esm/build/vendor-oxlint-plugins.d.ts +8 -0
- package/dist/esm/build/vendor-oxlint-plugins.js +53 -0
- package/dist/esm/cli/commands/audit-core.js +9 -1
- package/dist/esm/cli/commands/audit.js +16 -5
- package/dist/esm/cli/commands/blueprint/mutations.d.ts +15 -0
- package/dist/esm/cli/commands/blueprint/mutations.js +8 -2
- package/dist/esm/cli/commands/compile.d.ts +13 -0
- package/dist/esm/cli/commands/compile.js +182 -62
- package/dist/esm/cli/commands/format.js +17 -1
- package/dist/esm/cli/commands/gain/index.js +10 -3
- package/dist/esm/cli/commands/init/gitignore-patcher.js +6 -0
- package/dist/esm/cli/commands/init/index.js +29 -48
- package/dist/esm/cli/commands/init/mcp-spec.d.ts +54 -4
- package/dist/esm/cli/commands/init/mcp-spec.js +126 -22
- package/dist/esm/cli/commands/init/merge.js +30 -10
- package/dist/esm/cli/commands/init/scaffold-agents-md.js +5 -7
- package/dist/esm/cli/commands/init/scaffolders/codex-mcp/index.d.ts +56 -31
- package/dist/esm/cli/commands/init/scaffolders/codex-mcp/index.js +180 -43
- package/dist/esm/cli/commands/init/scaffolders/rtk/index.d.ts +17 -5
- package/dist/esm/cli/commands/init/scaffolders/rtk/index.js +56 -75
- package/dist/esm/cli/commands/init/scaffolders/runtime-check/index.js +7 -6
- package/dist/esm/cli/commands/init/untracked-collision.d.ts +12 -1
- package/dist/esm/cli/commands/init/untracked-collision.js +23 -1
- package/dist/esm/cli/commands/package-manager.js +8 -2
- package/dist/esm/cli/commands/pr-help.js +1 -1
- package/dist/esm/cli/commands/pr.js +18 -4
- package/dist/esm/cli/commands/review.js +60 -6
- package/dist/esm/cli/commands/worktree/router-dispatch.d.ts +24 -2
- package/dist/esm/cli/commands/worktree/router-dispatch.js +80 -48
- package/dist/esm/cli/commands/worktree/router.js +5 -2
- package/dist/esm/cli/direct-provider-launch.js +12 -4
- package/dist/esm/compiler/flatten.d.ts +10 -1
- package/dist/esm/compiler/flatten.js +14 -4
- package/dist/esm/compose/admin-blocks-registry-integrity.d.ts +22 -0
- package/dist/esm/compose/admin-blocks-registry-integrity.js +90 -0
- package/dist/esm/compose/compose.d.ts +46 -0
- package/dist/esm/compose/compose.js +250 -0
- package/dist/esm/compose/escape-html.d.ts +5 -0
- package/dist/esm/compose/escape-html.js +12 -0
- package/dist/esm/compose/ops-report-render.d.ts +5 -0
- package/dist/esm/compose/ops-report-render.js +71 -0
- package/dist/esm/compose/ops-report-schema.d.ts +42 -0
- package/dist/esm/compose/ops-report-schema.js +60 -0
- package/dist/esm/compose/preview.d.ts +14 -0
- package/dist/esm/compose/preview.js +32 -0
- package/dist/esm/compose/registry-resolve.d.ts +64 -0
- package/dist/esm/compose/registry-resolve.js +200 -0
- package/dist/esm/config/oxlint/graphql-conventions.js +2 -5
- package/dist/esm/config/oxlint/import-hygiene.js +21 -8
- package/dist/esm/config/oxlint/oxlint-config.fixture.d.ts +1 -1
- package/dist/esm/config/oxlint/oxlint-config.fixture.js +2 -3
- package/dist/esm/config/oxlint/oxlintrc.d.ts +5 -7
- package/dist/esm/config/oxlint/oxlintrc.js +18 -21
- package/dist/esm/config/oxlint/path-roles.js +53 -0
- package/dist/esm/config/oxlint/query-patterns.js +2 -5
- package/dist/esm/daemon/domains/blueprint-exec-ledger.d.ts +86 -0
- package/dist/esm/daemon/domains/blueprint-exec-ledger.js +134 -0
- package/dist/esm/daemon/domains/blueprint-exec.d.ts +106 -0
- package/dist/esm/daemon/domains/blueprint-exec.js +161 -0
- package/dist/esm/daemon/domains/launch.d.ts +25 -0
- package/dist/esm/daemon/domains/launch.js +30 -1
- package/dist/esm/daemon/handlers/blueprints-handlers.d.ts +12 -1
- package/dist/esm/daemon/handlers/blueprints-handlers.js +148 -6
- package/dist/esm/daemon/handlers/product.d.ts +10 -0
- package/dist/esm/daemon/handlers/product.js +10 -1
- package/dist/esm/daemon/methods.d.ts +21 -1
- package/dist/esm/daemon/methods.js +20 -0
- package/dist/esm/daemon/protocol/contract.d.ts +58 -0
- package/dist/esm/daemon/protocol/contract.js +38 -0
- package/dist/esm/daemon/server.js +7 -0
- package/dist/esm/docs-linter/blueprint-plan.js +2 -2
- package/dist/esm/errors/wp-error.js +21 -2
- package/dist/esm/git/changed-files.d.ts +20 -0
- package/dist/esm/git/changed-files.js +63 -0
- package/dist/esm/hooks/doctor.d.ts +11 -0
- package/dist/esm/hooks/doctor.js +114 -45
- package/dist/esm/hooks/pretool-guard/dev-routing.js +1 -1
- package/dist/esm/hooks/pretool-guard/rtk-rewrite.d.ts +24 -0
- package/dist/esm/hooks/pretool-guard/rtk-rewrite.js +50 -0
- package/dist/esm/hooks/pretool-guard/runner.js +6 -0
- package/dist/esm/hooks/status/index.js +1 -1
- package/dist/esm/hooks/stop/qa-changed-files.js +6 -1
- package/dist/esm/mcp/blueprint/_shared/lifecycle.js +39 -11
- package/dist/esm/mcp/blueprint/handlers/review-log.d.ts +14 -7
- package/dist/esm/mcp/blueprint/handlers/review-log.js +46 -22
- package/dist/esm/mcp/blueprint/handlers/validate.js +6 -0
- package/dist/esm/mcp/blueprint/registration.js +1 -1
- package/dist/esm/mcp/tools/_names.d.ts +1 -1
- package/dist/esm/mcp/tools/_names.js +4 -0
- package/dist/esm/mcp/tools/_registry.js +8 -0
- package/dist/esm/mcp/tools/_shared/redact.d.ts +22 -0
- package/dist/esm/mcp/tools/_shared/redact.js +114 -8
- package/dist/esm/mcp/tools/gain.js +28 -2
- package/dist/esm/mcp/tools/release-progress.d.ts +9 -0
- package/dist/esm/mcp/tools/release-progress.js +110 -0
- package/dist/esm/mcp/tools/worktree.js +65 -3
- package/dist/esm/mcp/tools/wp-ui-blocks-list.d.ts +3 -0
- package/dist/esm/mcp/tools/wp-ui-blocks-list.js +43 -0
- package/dist/esm/mcp/tools/wp-ui-compose.d.ts +3 -0
- package/dist/esm/mcp/tools/wp-ui-compose.js +96 -0
- package/dist/esm/mcp/tools/wp-ui-preview.d.ts +3 -0
- package/dist/esm/mcp/tools/wp-ui-preview.js +59 -0
- package/dist/esm/package.json +2 -0
- package/dist/esm/review/lifecycle-writers.d.ts +76 -0
- package/dist/esm/review/lifecycle-writers.js +330 -0
- package/dist/esm/review/subject.js +66 -32
- package/dist/esm/status/snapshot.d.ts +5 -0
- package/dist/esm/status/snapshot.js +27 -2
- package/dist/esm/test/shard-durations.json +0 -1
- package/dist/esm/test-helpers/global-setup.js +38 -3
- package/dist/esm/tool-runtime/managed-rtk.d.ts +44 -0
- package/dist/esm/tool-runtime/managed-rtk.js +275 -0
- package/dist/esm/tool-runtime/resolve-runner.js +18 -194
- package/dist/esm/utils/package-root.d.ts +32 -0
- package/dist/esm/utils/package-root.js +82 -0
- package/dist/esm/worktrees/git-metadata.d.ts +41 -0
- package/dist/esm/worktrees/git-metadata.js +48 -0
- package/package.json +27 -21
- package/dist/esm/config/oxlint/code-safety.d.ts +0 -18
- package/dist/esm/config/oxlint/foundation-purity.d.ts +0 -21
- package/dist/esm/config/oxlint/graphql-conventions.d.ts +0 -23
- package/dist/esm/config/oxlint/import-hygiene.d.ts +0 -34
- package/dist/esm/config/oxlint/index.d.ts +0 -22
- package/dist/esm/config/oxlint/index.js +0 -28
- package/dist/esm/config/oxlint/monorepo-paths.d.ts +0 -22
- package/dist/esm/config/oxlint/query-patterns.d.ts +0 -24
- package/dist/esm/config/oxlint/testing-quality.d.ts +0 -35
- package/dist/esm/config/oxlint/tier-boundaries.d.ts +0 -35
- package/dist/esm/output-transforms/rulesync.d.ts +0 -2
- package/dist/esm/output-transforms/rulesync.js +0 -79
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Blueprint exec run ledger: durable job ↔ PTY session ↔ blueprint attribution.
|
|
3
|
+
*
|
|
4
|
+
* Why this exists separately from the launch ledger: a launch handle is
|
|
5
|
+
* single-use and is consumed the moment Rust claims it, and the PTY reaper
|
|
6
|
+
* reports that *a* session ended but not which blueprint it belonged to.
|
|
7
|
+
* Neither can answer "what is this terminal running?" after a UI restart. A
|
|
8
|
+
* job id that outlives both can.
|
|
9
|
+
*
|
|
10
|
+
* Rows are deliberately env-free and argv-free: this ledger is readable by the
|
|
11
|
+
* webview, so it must never carry launch material.
|
|
12
|
+
*/
|
|
13
|
+
import { randomBytes } from "node:crypto";
|
|
14
|
+
export const BLUEPRINT_EXEC_JOB_STATES = ["preparing", "running", "terminal"];
|
|
15
|
+
const DEFAULT_MAX_JOBS = 256;
|
|
16
|
+
/**
|
|
17
|
+
* A job that was prepared but never bound to a session within this window is
|
|
18
|
+
* dead: its launch handle has long since expired (the launch ledger TTL is a
|
|
19
|
+
* minute), so no Rust peer can still claim it.
|
|
20
|
+
*/
|
|
21
|
+
const DEFAULT_STALE_UNBOUND_MS = 5 * 60_000;
|
|
22
|
+
export class BlueprintExecLedger {
|
|
23
|
+
byJobId = new Map();
|
|
24
|
+
/** FIFO of job ids that reached `terminal`, oldest first. */
|
|
25
|
+
terminalOrder = [];
|
|
26
|
+
maxJobs;
|
|
27
|
+
staleUnboundMs;
|
|
28
|
+
now;
|
|
29
|
+
constructor(options) {
|
|
30
|
+
this.maxJobs = options?.maxJobs ?? DEFAULT_MAX_JOBS;
|
|
31
|
+
this.staleUnboundMs = options?.staleUnboundMs ?? DEFAULT_STALE_UNBOUND_MS;
|
|
32
|
+
this.now = options?.now ?? Date.now;
|
|
33
|
+
}
|
|
34
|
+
/** Evict the oldest terminal job. O(1) amortized via the FIFO queue. */
|
|
35
|
+
evictOneTerminal() {
|
|
36
|
+
while (this.terminalOrder.length > 0) {
|
|
37
|
+
const candidate = this.terminalOrder.shift();
|
|
38
|
+
if (candidate === undefined)
|
|
39
|
+
break;
|
|
40
|
+
const job = this.byJobId.get(candidate);
|
|
41
|
+
// Skip stale queue entries whose job was already removed.
|
|
42
|
+
if (job === undefined)
|
|
43
|
+
continue;
|
|
44
|
+
if (job.state !== "terminal")
|
|
45
|
+
continue;
|
|
46
|
+
this.byJobId.delete(candidate);
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Last-resort reclaim: the oldest job that never bound a session and whose
|
|
53
|
+
* launch handle can no longer be claimed.
|
|
54
|
+
*
|
|
55
|
+
* Without this, a daemon that only ever prepared launches which were never
|
|
56
|
+
* claimed (a crashing Rust peer) would wedge at the cap forever, because
|
|
57
|
+
* nothing would ever transition those jobs to terminal.
|
|
58
|
+
*
|
|
59
|
+
* O(#jobs) and only on the eviction path when the terminal queue is empty.
|
|
60
|
+
*/
|
|
61
|
+
evictOneStaleUnbound() {
|
|
62
|
+
const cutoff = this.now() - this.staleUnboundMs;
|
|
63
|
+
for (const [jobId, job] of this.byJobId) {
|
|
64
|
+
if (job.sessionId === null && job.state === "preparing" && job.startedAt < cutoff) {
|
|
65
|
+
this.byJobId.delete(jobId);
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
register(input) {
|
|
72
|
+
if (this.byJobId.size >= this.maxJobs &&
|
|
73
|
+
!this.evictOneTerminal() &&
|
|
74
|
+
!this.evictOneStaleUnbound()) {
|
|
75
|
+
throw new Error(`blueprint exec ledger full (max ${this.maxJobs} tracked jobs)`);
|
|
76
|
+
}
|
|
77
|
+
const jobId = `bpexec-${Date.now().toString(36)}-${randomBytes(6).toString("hex")}`;
|
|
78
|
+
this.byJobId.set(jobId, {
|
|
79
|
+
jobId,
|
|
80
|
+
slug: input.slug,
|
|
81
|
+
taskId: input.taskId,
|
|
82
|
+
launchHandle: input.launchHandle,
|
|
83
|
+
sessionId: null,
|
|
84
|
+
startedAt: this.now(),
|
|
85
|
+
state: "preparing",
|
|
86
|
+
});
|
|
87
|
+
return { jobId };
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Bind the spawned PTY session to a job. Rust-only caller.
|
|
91
|
+
*
|
|
92
|
+
* Single-assignment: a second bind for the same job is rejected so a
|
|
93
|
+
* compromised or buggy peer cannot re-point an existing job at another
|
|
94
|
+
* session.
|
|
95
|
+
*/
|
|
96
|
+
bindSession(jobId, sessionId) {
|
|
97
|
+
const job = this.byJobId.get(jobId);
|
|
98
|
+
if (job === undefined) {
|
|
99
|
+
throw new Error("unknown blueprint exec job id");
|
|
100
|
+
}
|
|
101
|
+
if (job.sessionId !== null) {
|
|
102
|
+
throw new Error("blueprint exec job already bound to a session");
|
|
103
|
+
}
|
|
104
|
+
if (!Number.isSafeInteger(sessionId) || sessionId < 0) {
|
|
105
|
+
throw new Error("blueprint exec session id must be a non-negative integer");
|
|
106
|
+
}
|
|
107
|
+
const bound = { ...job, sessionId, state: "running" };
|
|
108
|
+
this.byJobId.set(jobId, bound);
|
|
109
|
+
return bound;
|
|
110
|
+
}
|
|
111
|
+
markTerminal(jobId) {
|
|
112
|
+
const job = this.byJobId.get(jobId);
|
|
113
|
+
if (job === undefined)
|
|
114
|
+
return null;
|
|
115
|
+
if (job.state === "terminal")
|
|
116
|
+
return job;
|
|
117
|
+
const done = { ...job, state: "terminal" };
|
|
118
|
+
this.byJobId.set(jobId, done);
|
|
119
|
+
this.terminalOrder.push(jobId);
|
|
120
|
+
return done;
|
|
121
|
+
}
|
|
122
|
+
get(jobId) {
|
|
123
|
+
return this.byJobId.get(jobId);
|
|
124
|
+
}
|
|
125
|
+
list() {
|
|
126
|
+
return [...this.byJobId.values()];
|
|
127
|
+
}
|
|
128
|
+
size() {
|
|
129
|
+
return this.byJobId.size;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
export function createBlueprintExecLedger(options) {
|
|
133
|
+
return new BlueprintExecLedger(options);
|
|
134
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Blueprint exec launch-spec composition (desktop "exec a blueprint" seam).
|
|
3
|
+
*
|
|
4
|
+
* Two invariants this module exists to protect:
|
|
5
|
+
*
|
|
6
|
+
* 1. **Exec params are data, not command.** The webview may supply only a
|
|
7
|
+
* blueprint slug and an optional task id. Both are validated against
|
|
8
|
+
* anchored, bounded patterns, and the resolved blueprint folder is
|
|
9
|
+
* path-confined against the daemon's allowlisted roots. The executor
|
|
10
|
+
* program, argv, cwd, and env are composed *here*, daemon-side. No
|
|
11
|
+
* caller-supplied program, flag, path, or env value ever reaches a spawn
|
|
12
|
+
* line, and argv is always an ARRAY — never a shell string.
|
|
13
|
+
* 2. **Spawning is Rust-only.** Nothing in this module spawns a process; it
|
|
14
|
+
* returns a spec that Rust claims and spawns through the PTY manager. No
|
|
15
|
+
* process-spawning API is imported here, and a test enforces that.
|
|
16
|
+
*
|
|
17
|
+
* Execution status is read from blueprint frontmatter metadata written by the
|
|
18
|
+
* executor itself (`#execution/metadata.js`), never inferred from PTY output.
|
|
19
|
+
*/
|
|
20
|
+
import { type PathConfineFs } from "#daemon/path-confinement.js";
|
|
21
|
+
import { type BlueprintExecutionMetadata } from "#execution/metadata.js";
|
|
22
|
+
/**
|
|
23
|
+
* Lifecycle directories searched for a slug, most-active first.
|
|
24
|
+
*
|
|
25
|
+
* Fixed-size list: slug resolution is O(1) directory probes, never a walk.
|
|
26
|
+
*/
|
|
27
|
+
export declare const BLUEPRINT_LIFECYCLE_DIRS: readonly ["in-progress", "planned", "draft", "completed", "parked", "archived"];
|
|
28
|
+
/** Upper bound on an accepted slug, mirrored by {@link BLUEPRINT_SLUG_PATTERN}. */
|
|
29
|
+
export declare const MAX_BLUEPRINT_SLUG_LENGTH = 64;
|
|
30
|
+
/**
|
|
31
|
+
* Anchored, linear-time (no nested quantifier) slug pattern. Rejects `..`,
|
|
32
|
+
* `/`, `\`, leading `-`, uppercase, whitespace, NUL, and every shell
|
|
33
|
+
* metacharacter by construction rather than by blocklist.
|
|
34
|
+
*/
|
|
35
|
+
export declare const BLUEPRINT_SLUG_PATTERN: RegExp;
|
|
36
|
+
/** Dotted blueprint task id, e.g. `1.2` or `10.3.1`. */
|
|
37
|
+
export declare const BLUEPRINT_TASK_ID_PATTERN: RegExp;
|
|
38
|
+
/** Rejected caller input. Carries no attacker-controlled text. */
|
|
39
|
+
export declare class BlueprintExecValidationError extends Error {
|
|
40
|
+
constructor(message: string);
|
|
41
|
+
}
|
|
42
|
+
export type BlueprintExecSpec = {
|
|
43
|
+
/** Executor program path, resolved daemon-side. */
|
|
44
|
+
readonly command: string;
|
|
45
|
+
/** Full argv array (excluding `command`). Never a shell string. */
|
|
46
|
+
readonly args: readonly string[];
|
|
47
|
+
readonly cwd: string;
|
|
48
|
+
/** Allowlist-filtered env, safe to hand to Rust. */
|
|
49
|
+
readonly env: Readonly<Record<string, string>>;
|
|
50
|
+
readonly slug: string;
|
|
51
|
+
readonly taskId: string | null;
|
|
52
|
+
/** Confined absolute path of the blueprint folder. */
|
|
53
|
+
readonly blueprintDir: string;
|
|
54
|
+
readonly lifecycleStatus: string;
|
|
55
|
+
};
|
|
56
|
+
export type ComposeBlueprintExecSpecInput = {
|
|
57
|
+
readonly slug: string;
|
|
58
|
+
readonly taskId?: string | null;
|
|
59
|
+
readonly repoRoot: string;
|
|
60
|
+
readonly allowlistedRoots: readonly string[];
|
|
61
|
+
/** Base env; defaults to `process.env`. Always allowlist-filtered. */
|
|
62
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
63
|
+
readonly fs?: PathConfineFs;
|
|
64
|
+
/** Injectable existence probe (tests); defaults to `existsSync`. */
|
|
65
|
+
readonly exists?: (path: string) => boolean;
|
|
66
|
+
/**
|
|
67
|
+
* Injectable executor invocation prefix (tests). Production resolves the
|
|
68
|
+
* stable `wp` launcher via {@link buildSelfInvocationCommand}.
|
|
69
|
+
*/
|
|
70
|
+
readonly selfCommand?: readonly string[];
|
|
71
|
+
/**
|
|
72
|
+
* Injectable probe for the resolved executor binary. Defaults to
|
|
73
|
+
* `existsSync`; tests that inject a synthetic `selfCommand` stub this.
|
|
74
|
+
*/
|
|
75
|
+
readonly commandExists?: (path: string) => boolean;
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* Validate a caller-supplied blueprint slug.
|
|
79
|
+
*
|
|
80
|
+
* O(len) with an anchored, backtracking-free pattern on a length-capped input.
|
|
81
|
+
* The rejected value is never echoed back into the error.
|
|
82
|
+
*/
|
|
83
|
+
export declare function assertValidBlueprintSlug(slug: unknown): string;
|
|
84
|
+
/** Validate an optional dotted task id. O(len) on a bounded input. */
|
|
85
|
+
export declare function assertValidBlueprintTaskId(taskId: unknown): string | null;
|
|
86
|
+
/**
|
|
87
|
+
* Compose the executor launch spec for a validated slug.
|
|
88
|
+
*
|
|
89
|
+
* Hot-path complexity: O(len(slug)) validation + O(1) lifecycle probes +
|
|
90
|
+
* O(|allowlist|) env filtering, where the env allowlist is a fixed constant
|
|
91
|
+
* set. No input-proportional nesting anywhere.
|
|
92
|
+
*/
|
|
93
|
+
export declare function composeBlueprintExecSpec(input: ComposeBlueprintExecSpecInput): BlueprintExecSpec;
|
|
94
|
+
/**
|
|
95
|
+
* Read executor-written progress for a blueprint.
|
|
96
|
+
*
|
|
97
|
+
* Source of truth is the blueprint's own frontmatter execution metadata, which
|
|
98
|
+
* the executor maintains. PTY bytes are never read here — the terminal stream
|
|
99
|
+
* is human-viewing only and is not a status input.
|
|
100
|
+
*
|
|
101
|
+
* Returns `null` when the blueprint has no execution metadata yet.
|
|
102
|
+
*/
|
|
103
|
+
export declare function readBlueprintExecProgress(repoRoot: string, slug: string, options?: {
|
|
104
|
+
readonly exists?: (path: string) => boolean;
|
|
105
|
+
readonly readFile?: (path: string) => string;
|
|
106
|
+
}): BlueprintExecutionMetadata | null;
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Blueprint exec launch-spec composition (desktop "exec a blueprint" seam).
|
|
3
|
+
*
|
|
4
|
+
* Two invariants this module exists to protect:
|
|
5
|
+
*
|
|
6
|
+
* 1. **Exec params are data, not command.** The webview may supply only a
|
|
7
|
+
* blueprint slug and an optional task id. Both are validated against
|
|
8
|
+
* anchored, bounded patterns, and the resolved blueprint folder is
|
|
9
|
+
* path-confined against the daemon's allowlisted roots. The executor
|
|
10
|
+
* program, argv, cwd, and env are composed *here*, daemon-side. No
|
|
11
|
+
* caller-supplied program, flag, path, or env value ever reaches a spawn
|
|
12
|
+
* line, and argv is always an ARRAY — never a shell string.
|
|
13
|
+
* 2. **Spawning is Rust-only.** Nothing in this module spawns a process; it
|
|
14
|
+
* returns a spec that Rust claims and spawns through the PTY manager. No
|
|
15
|
+
* process-spawning API is imported here, and a test enforces that.
|
|
16
|
+
*
|
|
17
|
+
* Execution status is read from blueprint frontmatter metadata written by the
|
|
18
|
+
* executor itself (`#execution/metadata.js`), never inferred from PTY output.
|
|
19
|
+
*/
|
|
20
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
21
|
+
import { join } from "node:path";
|
|
22
|
+
import { buildSelfInvocationCommand } from "#cli/auto-update/self-invocation.js";
|
|
23
|
+
import { filterAllowlistedEnv } from "#daemon/launch/env-allowlist.js";
|
|
24
|
+
import { confinePaths } from "#daemon/path-confinement.js";
|
|
25
|
+
import { readBlueprintExecutionMetadata, } from "#execution/metadata.js";
|
|
26
|
+
/**
|
|
27
|
+
* Lifecycle directories searched for a slug, most-active first.
|
|
28
|
+
*
|
|
29
|
+
* Fixed-size list: slug resolution is O(1) directory probes, never a walk.
|
|
30
|
+
*/
|
|
31
|
+
export const BLUEPRINT_LIFECYCLE_DIRS = [
|
|
32
|
+
"in-progress",
|
|
33
|
+
"planned",
|
|
34
|
+
"draft",
|
|
35
|
+
"completed",
|
|
36
|
+
"parked",
|
|
37
|
+
"archived",
|
|
38
|
+
];
|
|
39
|
+
/** Upper bound on an accepted slug, mirrored by {@link BLUEPRINT_SLUG_PATTERN}. */
|
|
40
|
+
export const MAX_BLUEPRINT_SLUG_LENGTH = 64;
|
|
41
|
+
/**
|
|
42
|
+
* Anchored, linear-time (no nested quantifier) slug pattern. Rejects `..`,
|
|
43
|
+
* `/`, `\`, leading `-`, uppercase, whitespace, NUL, and every shell
|
|
44
|
+
* metacharacter by construction rather than by blocklist.
|
|
45
|
+
*/
|
|
46
|
+
export const BLUEPRINT_SLUG_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/u;
|
|
47
|
+
/** Dotted blueprint task id, e.g. `1.2` or `10.3.1`. */
|
|
48
|
+
export const BLUEPRINT_TASK_ID_PATTERN = /^[0-9]+(?:\.[0-9]+)+$/u;
|
|
49
|
+
/** Rejected caller input. Carries no attacker-controlled text. */
|
|
50
|
+
export class BlueprintExecValidationError extends Error {
|
|
51
|
+
constructor(message) {
|
|
52
|
+
super(message);
|
|
53
|
+
this.name = "BlueprintExecValidationError";
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Validate a caller-supplied blueprint slug.
|
|
58
|
+
*
|
|
59
|
+
* O(len) with an anchored, backtracking-free pattern on a length-capped input.
|
|
60
|
+
* The rejected value is never echoed back into the error.
|
|
61
|
+
*/
|
|
62
|
+
export function assertValidBlueprintSlug(slug) {
|
|
63
|
+
if (typeof slug !== "string" || slug.length > MAX_BLUEPRINT_SLUG_LENGTH) {
|
|
64
|
+
throw new BlueprintExecValidationError("blueprint slug must be a bounded string");
|
|
65
|
+
}
|
|
66
|
+
if (!BLUEPRINT_SLUG_PATTERN.test(slug)) {
|
|
67
|
+
throw new BlueprintExecValidationError("blueprint slug must match ^[a-z0-9][a-z0-9-]{0,63}$");
|
|
68
|
+
}
|
|
69
|
+
return slug;
|
|
70
|
+
}
|
|
71
|
+
/** Validate an optional dotted task id. O(len) on a bounded input. */
|
|
72
|
+
export function assertValidBlueprintTaskId(taskId) {
|
|
73
|
+
if (taskId === undefined || taskId === null || taskId === "")
|
|
74
|
+
return null;
|
|
75
|
+
if (typeof taskId !== "string" || taskId.length > 32) {
|
|
76
|
+
throw new BlueprintExecValidationError("blueprint task id must be a bounded string");
|
|
77
|
+
}
|
|
78
|
+
if (!BLUEPRINT_TASK_ID_PATTERN.test(taskId)) {
|
|
79
|
+
throw new BlueprintExecValidationError("blueprint task id must match ^[0-9]+(\\.[0-9]+)+$");
|
|
80
|
+
}
|
|
81
|
+
return taskId;
|
|
82
|
+
}
|
|
83
|
+
function locateBlueprintDir(repoRoot, slug, exists) {
|
|
84
|
+
for (const lifecycleStatus of BLUEPRINT_LIFECYCLE_DIRS) {
|
|
85
|
+
const dir = join(repoRoot, "blueprints", lifecycleStatus, slug);
|
|
86
|
+
if (exists(dir))
|
|
87
|
+
return { dir, lifecycleStatus };
|
|
88
|
+
}
|
|
89
|
+
throw new BlueprintExecValidationError("no blueprint folder found for the requested slug");
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Compose the executor launch spec for a validated slug.
|
|
93
|
+
*
|
|
94
|
+
* Hot-path complexity: O(len(slug)) validation + O(1) lifecycle probes +
|
|
95
|
+
* O(|allowlist|) env filtering, where the env allowlist is a fixed constant
|
|
96
|
+
* set. No input-proportional nesting anywhere.
|
|
97
|
+
*/
|
|
98
|
+
export function composeBlueprintExecSpec(input) {
|
|
99
|
+
const slug = assertValidBlueprintSlug(input.slug);
|
|
100
|
+
const taskId = assertValidBlueprintTaskId(input.taskId);
|
|
101
|
+
const exists = input.exists ?? existsSync;
|
|
102
|
+
const located = locateBlueprintDir(input.repoRoot, slug, exists);
|
|
103
|
+
const confined = confinePaths({ repoRoot: input.repoRoot, blueprintDir: located.dir }, input.allowlistedRoots, input.fs);
|
|
104
|
+
if (!confined.ok) {
|
|
105
|
+
throw new BlueprintExecValidationError(`blueprint exec path rejected: ${confined.reason}`);
|
|
106
|
+
}
|
|
107
|
+
const cwd = confined.resolvedPaths.repoRoot ?? input.repoRoot;
|
|
108
|
+
const blueprintDir = confined.resolvedPaths.blueprintDir ?? located.dir;
|
|
109
|
+
// Daemon-authored argv. The only caller-derived element is the validated
|
|
110
|
+
// slug, and it can never begin with `-` so it cannot become a flag.
|
|
111
|
+
const execArgs = ["blueprint", "exec", slug];
|
|
112
|
+
const invocation = input.selfCommand === undefined
|
|
113
|
+
? buildSelfInvocationCommand([...execArgs])
|
|
114
|
+
: [...input.selfCommand, ...execArgs];
|
|
115
|
+
const [command, ...args] = invocation;
|
|
116
|
+
if (command === undefined || command.length === 0) {
|
|
117
|
+
throw new BlueprintExecValidationError("could not resolve the wp executor binary");
|
|
118
|
+
}
|
|
119
|
+
// Fail fast with a readable reason instead of preparing a launch whose PTY
|
|
120
|
+
// would die immediately with an opaque spawn error.
|
|
121
|
+
const commandExists = input.commandExists ?? existsSync;
|
|
122
|
+
if (command.includes("/") && !commandExists(command)) {
|
|
123
|
+
throw new BlueprintExecValidationError("the resolved wp executor binary does not exist on this system");
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
command,
|
|
127
|
+
args,
|
|
128
|
+
cwd,
|
|
129
|
+
env: filterAllowlistedEnv(input.env ?? process.env),
|
|
130
|
+
slug,
|
|
131
|
+
taskId,
|
|
132
|
+
blueprintDir,
|
|
133
|
+
lifecycleStatus: located.lifecycleStatus,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Read executor-written progress for a blueprint.
|
|
138
|
+
*
|
|
139
|
+
* Source of truth is the blueprint's own frontmatter execution metadata, which
|
|
140
|
+
* the executor maintains. PTY bytes are never read here — the terminal stream
|
|
141
|
+
* is human-viewing only and is not a status input.
|
|
142
|
+
*
|
|
143
|
+
* Returns `null` when the blueprint has no execution metadata yet.
|
|
144
|
+
*/
|
|
145
|
+
export function readBlueprintExecProgress(repoRoot, slug, options) {
|
|
146
|
+
const validSlug = assertValidBlueprintSlug(slug);
|
|
147
|
+
const exists = options?.exists ?? existsSync;
|
|
148
|
+
const readFile = options?.readFile ?? ((path) => readFileSync(path, "utf8"));
|
|
149
|
+
for (const lifecycleStatus of BLUEPRINT_LIFECYCLE_DIRS) {
|
|
150
|
+
const overview = join(repoRoot, "blueprints", lifecycleStatus, validSlug, "_overview.md");
|
|
151
|
+
if (!exists(overview))
|
|
152
|
+
continue;
|
|
153
|
+
try {
|
|
154
|
+
return readBlueprintExecutionMetadata(readFile(overview));
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
@@ -21,6 +21,17 @@ export type PrepareLaunchInput = {
|
|
|
21
21
|
/** When null/omitted, assembly uses profile none (no secret-provider fetch). */
|
|
22
22
|
readonly secretsConfig?: import("#runtime/secrets-config.js").SecretsConfig | null;
|
|
23
23
|
};
|
|
24
|
+
/**
|
|
25
|
+
* A launch spec the daemon composed itself (not via provider assembly).
|
|
26
|
+
* `args` is always an array — the ledger never accepts a shell string.
|
|
27
|
+
*/
|
|
28
|
+
export type PrepareComposedLaunchInput = {
|
|
29
|
+
readonly command: string;
|
|
30
|
+
readonly args: readonly string[];
|
|
31
|
+
readonly cwd: string;
|
|
32
|
+
readonly env: Readonly<Record<string, string | undefined>>;
|
|
33
|
+
readonly resolvedSecretKeys?: readonly string[];
|
|
34
|
+
};
|
|
24
35
|
export declare class LaunchLedger {
|
|
25
36
|
private readonly byHandle;
|
|
26
37
|
private readonly ttlMs;
|
|
@@ -33,6 +44,20 @@ export declare class LaunchLedger {
|
|
|
33
44
|
prepare(input: PrepareLaunchInput): {
|
|
34
45
|
readonly handle: string;
|
|
35
46
|
};
|
|
47
|
+
/**
|
|
48
|
+
* Register an already-composed spec (e.g. a blueprint exec launch) under the
|
|
49
|
+
* same opaque-handle / single-use-claim contract as {@link prepare}.
|
|
50
|
+
*
|
|
51
|
+
* Used when the command is not an agent provider and so cannot go through
|
|
52
|
+
* provider env assembly. The env is re-filtered through the mandatory
|
|
53
|
+
* allowlist here as well, so this entry point can never widen delivery
|
|
54
|
+
* beyond what `prepare` would allow even if a caller passes a raw map.
|
|
55
|
+
*
|
|
56
|
+
* O(1) apart from the shared TTL purge.
|
|
57
|
+
*/
|
|
58
|
+
prepareComposed(input: PrepareComposedLaunchInput): {
|
|
59
|
+
readonly handle: string;
|
|
60
|
+
};
|
|
36
61
|
/**
|
|
37
62
|
* Single-use claim for the Rust peer. Removes the entry so env is not retained.
|
|
38
63
|
* Returns null if unknown, expired, or already claimed.
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Returns opaque handles to UI; raw env is only claimable by the rust peer.
|
|
4
4
|
*/
|
|
5
5
|
import { randomBytes } from "node:crypto";
|
|
6
|
-
import { assembleAllowlistedLaunchEnv, } from "#daemon/launch/env-allowlist.js";
|
|
6
|
+
import { assembleAllowlistedLaunchEnv, filterAllowlistedEnv, } from "#daemon/launch/env-allowlist.js";
|
|
7
7
|
const DEFAULT_TTL_MS = 60_000;
|
|
8
8
|
const DEFAULT_MAX_UNCLAIMED = 64;
|
|
9
9
|
export class LaunchLedger {
|
|
@@ -45,6 +45,35 @@ export class LaunchLedger {
|
|
|
45
45
|
});
|
|
46
46
|
return { handle };
|
|
47
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Register an already-composed spec (e.g. a blueprint exec launch) under the
|
|
50
|
+
* same opaque-handle / single-use-claim contract as {@link prepare}.
|
|
51
|
+
*
|
|
52
|
+
* Used when the command is not an agent provider and so cannot go through
|
|
53
|
+
* provider env assembly. The env is re-filtered through the mandatory
|
|
54
|
+
* allowlist here as well, so this entry point can never widen delivery
|
|
55
|
+
* beyond what `prepare` would allow even if a caller passes a raw map.
|
|
56
|
+
*
|
|
57
|
+
* O(1) apart from the shared TTL purge.
|
|
58
|
+
*/
|
|
59
|
+
prepareComposed(input) {
|
|
60
|
+
this.purgeExpired();
|
|
61
|
+
if (this.byHandle.size >= this.maxUnclaimed) {
|
|
62
|
+
throw new Error(`launch ledger full (max ${this.maxUnclaimed} unclaimed handles)`);
|
|
63
|
+
}
|
|
64
|
+
const handle = `launch-${Date.now().toString(36)}-${randomBytes(6).toString("hex")}`;
|
|
65
|
+
this.byHandle.set(handle, {
|
|
66
|
+
handle,
|
|
67
|
+
preparedAt: Date.now(),
|
|
68
|
+
command: input.command,
|
|
69
|
+
args: [...input.args],
|
|
70
|
+
cwd: input.cwd,
|
|
71
|
+
env: filterAllowlistedEnv(input.env, {
|
|
72
|
+
resolvedSecretKeys: input.resolvedSecretKeys,
|
|
73
|
+
}),
|
|
74
|
+
});
|
|
75
|
+
return { handle };
|
|
76
|
+
}
|
|
48
77
|
/**
|
|
49
78
|
* Single-use claim for the Rust peer. Removes the entry so env is not retained.
|
|
50
79
|
* Returns null if unknown, expired, or already claimed.
|
|
@@ -1,5 +1,16 @@
|
|
|
1
|
+
import type { BlueprintExecLedger } from "#daemon/domains/blueprint-exec-ledger.js";
|
|
2
|
+
import type { LaunchLedger } from "#daemon/domains/launch.js";
|
|
1
3
|
import type { MethodHandler } from "#daemon/rpc.js";
|
|
4
|
+
export type BlueprintsHandlerDeps = {
|
|
5
|
+
readonly launchLedger?: LaunchLedger;
|
|
6
|
+
readonly execLedger?: BlueprintExecLedger;
|
|
7
|
+
readonly allowlistedRoots?: readonly string[];
|
|
8
|
+
};
|
|
2
9
|
/**
|
|
3
10
|
* Build blueprint-domain handlers scoped to `repoRoot`.
|
|
11
|
+
*
|
|
12
|
+
* The ledgers are created once per daemon (not per connection) and passed in,
|
|
13
|
+
* because the webview peer registers the job while the rust peer binds the
|
|
14
|
+
* session — two different connections must observe the same ledger.
|
|
4
15
|
*/
|
|
5
|
-
export declare function createBlueprintsHandlers(repoRoot: string): Readonly<Partial<Record<string, MethodHandler>>>;
|
|
16
|
+
export declare function createBlueprintsHandlers(repoRoot: string, deps?: BlueprintsHandlerDeps): Readonly<Partial<Record<string, MethodHandler>>>;
|
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Blueprint-domain product handlers
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Blueprint-domain product handlers.
|
|
3
|
+
*
|
|
4
|
+
* - `blueprints.list` enumerates the `blueprints/<status>/` directories.
|
|
5
|
+
* - `blueprints.exec` composes an executor launch spec and prepares it as an
|
|
6
|
+
* opaque, single-use launch handle. It returns **only** identifiers: no
|
|
7
|
+
* command, no argv, no cwd, no env ever crosses to the webview, and this
|
|
8
|
+
* module never spawns anything (spawning is Rust-only, through the PTY
|
|
9
|
+
* manager, after a rust-credentialed `launch.claim`).
|
|
10
|
+
* - `blueprints.execBind` is rust-role-only and records the spawned PTY
|
|
11
|
+
* session against the job so attribution survives a UI restart.
|
|
12
|
+
* - `blueprints.execStatus` joins ledger rows with executor-written blueprint
|
|
13
|
+
* frontmatter metadata. Status is never parsed out of PTY output — the
|
|
14
|
+
* terminal stream is human-viewing only.
|
|
5
15
|
*/
|
|
6
16
|
import { readdirSync, existsSync, statSync } from "node:fs";
|
|
7
17
|
import { join } from "node:path";
|
|
18
|
+
import { composeBlueprintExecSpec, readBlueprintExecProgress, } from "#daemon/domains/blueprint-exec.js";
|
|
19
|
+
import { asRecord, requireString } from "./shared.js";
|
|
8
20
|
function listBlueprintRows(repoRoot) {
|
|
9
21
|
const statuses = ["completed", "in-progress", "planned", "parked", "draft", "archived"];
|
|
10
22
|
const rows = [];
|
|
@@ -21,16 +33,146 @@ function listBlueprintRows(repoRoot) {
|
|
|
21
33
|
rows.sort((a, b) => a.slug.localeCompare(b.slug));
|
|
22
34
|
return rows;
|
|
23
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Executor statuses that mean the run is over. Mirrors `executionStatusSchema`
|
|
38
|
+
* terminal values; a job in one of these retires from the ledger.
|
|
39
|
+
*/
|
|
40
|
+
const TERMINAL_EXECUTION_STATUSES = new Set([
|
|
41
|
+
"completed",
|
|
42
|
+
"failed",
|
|
43
|
+
"stopped",
|
|
44
|
+
]);
|
|
45
|
+
function optionalString(record, key) {
|
|
46
|
+
const value = record[key];
|
|
47
|
+
if (value === undefined || value === null || value === "")
|
|
48
|
+
return null;
|
|
49
|
+
if (typeof value !== "string") {
|
|
50
|
+
throw new Error(`param ${key} must be a string when present`);
|
|
51
|
+
}
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Params `blueprints.exec` accepts. `requestNonce`/`confirmationToken` belong
|
|
56
|
+
* to the auth envelope and may still be present on the domain record.
|
|
57
|
+
*
|
|
58
|
+
* The protocol contract's strict Zod schema is a declarative helper and is not
|
|
59
|
+
* applied on the dispatch path, so unknown keys would otherwise be silently
|
|
60
|
+
* ignored rather than refused. Ignoring is already fail-safe here (the handler
|
|
61
|
+
* reads only known keys), but refusing is what makes "exec params are data,
|
|
62
|
+
* not command" an enforced boundary instead of an aspirational one — and it
|
|
63
|
+
* stops a future edit that naively spreads params from becoming an injection.
|
|
64
|
+
*/
|
|
65
|
+
const EXEC_ALLOWED_PARAM_KEYS = new Set([
|
|
66
|
+
"slug",
|
|
67
|
+
"taskId",
|
|
68
|
+
"confirmationToken",
|
|
69
|
+
"requestNonce",
|
|
70
|
+
]);
|
|
71
|
+
/** O(#keys) on a bounded record. Never echoes caller-controlled key text. */
|
|
72
|
+
function assertNoSmuggledExecParams(record) {
|
|
73
|
+
for (const key of Object.keys(record)) {
|
|
74
|
+
if (!EXEC_ALLOWED_PARAM_KEYS.has(key)) {
|
|
75
|
+
throw new Error("unexpected param for blueprints.exec: only slug and taskId are accepted " +
|
|
76
|
+
"(command, args, cwd, and env are composed by the daemon)");
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function requireSessionId(record) {
|
|
81
|
+
const value = record["sessionId"];
|
|
82
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
|
|
83
|
+
throw new Error("missing or invalid param: sessionId");
|
|
84
|
+
}
|
|
85
|
+
return value;
|
|
86
|
+
}
|
|
87
|
+
function requireExecDeps(deps) {
|
|
88
|
+
const { launchLedger, execLedger } = deps;
|
|
89
|
+
if (launchLedger === undefined || execLedger === undefined) {
|
|
90
|
+
throw new Error("blueprints exec is unavailable: daemon ledgers are not wired");
|
|
91
|
+
}
|
|
92
|
+
return { launchLedger, execLedger };
|
|
93
|
+
}
|
|
24
94
|
/**
|
|
25
95
|
* Build blueprint-domain handlers scoped to `repoRoot`.
|
|
96
|
+
*
|
|
97
|
+
* The ledgers are created once per daemon (not per connection) and passed in,
|
|
98
|
+
* because the webview peer registers the job while the rust peer binds the
|
|
99
|
+
* session — two different connections must observe the same ledger.
|
|
26
100
|
*/
|
|
27
|
-
export function createBlueprintsHandlers(repoRoot) {
|
|
101
|
+
export function createBlueprintsHandlers(repoRoot, deps = {}) {
|
|
102
|
+
const allowlistedRoots = deps.allowlistedRoots !== undefined && deps.allowlistedRoots.length > 0
|
|
103
|
+
? deps.allowlistedRoots
|
|
104
|
+
: [repoRoot];
|
|
28
105
|
return {
|
|
29
106
|
"blueprints.list": () => ({
|
|
30
107
|
blueprints: listBlueprintRows(repoRoot),
|
|
31
108
|
}),
|
|
32
|
-
"blueprints.exec": () => {
|
|
33
|
-
|
|
109
|
+
"blueprints.exec": (_session, params) => {
|
|
110
|
+
const { launchLedger, execLedger } = requireExecDeps(deps);
|
|
111
|
+
const record = asRecord(params);
|
|
112
|
+
assertNoSmuggledExecParams(record);
|
|
113
|
+
const slug = requireString(record, "slug");
|
|
114
|
+
const taskId = optionalString(record, "taskId");
|
|
115
|
+
// Validation, path confinement, and daemon-side argv composition.
|
|
116
|
+
const spec = composeBlueprintExecSpec({
|
|
117
|
+
slug,
|
|
118
|
+
taskId,
|
|
119
|
+
repoRoot,
|
|
120
|
+
allowlistedRoots,
|
|
121
|
+
});
|
|
122
|
+
const { handle } = launchLedger.prepareComposed({
|
|
123
|
+
command: spec.command,
|
|
124
|
+
args: spec.args,
|
|
125
|
+
cwd: spec.cwd,
|
|
126
|
+
env: spec.env,
|
|
127
|
+
});
|
|
128
|
+
const { jobId } = execLedger.register({
|
|
129
|
+
slug: spec.slug,
|
|
130
|
+
taskId: spec.taskId,
|
|
131
|
+
launchHandle: handle,
|
|
132
|
+
});
|
|
133
|
+
// Opaque only: the rust-role claim is what yields command/args/env.
|
|
134
|
+
return { jobId, handle };
|
|
135
|
+
},
|
|
136
|
+
"blueprints.execBind": (_session, params) => {
|
|
137
|
+
const { execLedger } = requireExecDeps(deps);
|
|
138
|
+
const record = asRecord(params);
|
|
139
|
+
const jobId = requireString(record, "jobId");
|
|
140
|
+
const sessionId = requireSessionId(record);
|
|
141
|
+
const job = execLedger.bindSession(jobId, sessionId);
|
|
142
|
+
return { jobId: job.jobId, sessionId: job.sessionId, state: job.state };
|
|
143
|
+
},
|
|
144
|
+
"blueprints.execStatus": (_session, params) => {
|
|
145
|
+
const { execLedger } = requireExecDeps(deps);
|
|
146
|
+
const record = asRecord(params);
|
|
147
|
+
const slugFilter = optionalString(record, "slug");
|
|
148
|
+
const jobs = execLedger
|
|
149
|
+
.list()
|
|
150
|
+
.filter((job) => slugFilter === null || job.slug === slugFilter)
|
|
151
|
+
.map((job) => {
|
|
152
|
+
// Executor-written frontmatter metadata — never PTY bytes.
|
|
153
|
+
const progress = readBlueprintExecProgress(repoRoot, job.slug);
|
|
154
|
+
// Drive the terminal transition from the executor's own status.
|
|
155
|
+
// This is what keeps `markTerminal` (and therefore ledger eviction)
|
|
156
|
+
// live in production without inventing a push transport: the daemon
|
|
157
|
+
// has no server-push seam, so the pull that reports status is also
|
|
158
|
+
// the pull that retires the job.
|
|
159
|
+
const settled = progress !== null && TERMINAL_EXECUTION_STATUSES.has(progress.status)
|
|
160
|
+
? execLedger.markTerminal(job.jobId)
|
|
161
|
+
: null;
|
|
162
|
+
const state = settled?.state ?? job.state;
|
|
163
|
+
return {
|
|
164
|
+
jobId: job.jobId,
|
|
165
|
+
slug: job.slug,
|
|
166
|
+
taskId: job.taskId,
|
|
167
|
+
sessionId: job.sessionId,
|
|
168
|
+
state,
|
|
169
|
+
startedAt: job.startedAt,
|
|
170
|
+
executionId: progress?.executionId ?? null,
|
|
171
|
+
executionStatus: progress?.status ?? null,
|
|
172
|
+
executionUpdatedAt: progress?.updatedAt ?? null,
|
|
173
|
+
};
|
|
174
|
+
});
|
|
175
|
+
return { jobs };
|
|
34
176
|
},
|
|
35
177
|
};
|
|
36
178
|
}
|