@webpresso/agent-kit 3.3.3 → 3.3.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/THIRD-PARTY-NOTICES.md +12 -10
- 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/dist/esm/audit/agents.js +126 -26
- 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 +38 -12
- 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 +63 -10
- 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/dossier.js +41 -1
- 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/vendor-oxlint-plugins.d.ts +11 -0
- package/dist/esm/build/vendor-oxlint-plugins.js +48 -0
- 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 +32 -52
- package/dist/esm/cli/commands/init/mcp-spec.d.ts +48 -13
- package/dist/esm/cli/commands/init/mcp-spec.js +86 -30
- package/dist/esm/cli/commands/init/merge.js +30 -10
- 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/pr-help.js +1 -1
- package/dist/esm/cli/commands/pr.js +18 -4
- package/dist/esm/cli/commands/review.d.ts +1 -0
- package/dist/esm/cli/commands/review.js +137 -7
- package/dist/esm/cli/commands/sync.d.ts +8 -6
- package/dist/esm/cli/commands/sync.js +11 -9
- 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 +8 -0
- package/dist/esm/compose/compose.js +115 -8
- package/dist/esm/compose/ops-report-schema.js +17 -3
- package/dist/esm/compose/registry-resolve.d.ts +17 -0
- package/dist/esm/compose/registry-resolve.js +78 -11
- 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 +19 -31
- package/dist/esm/config/oxlint/path-roles.js +53 -0
- package/dist/esm/config/oxlint/query-patterns.js +2 -5
- package/dist/esm/content/dispatch.js +4 -0
- package/dist/esm/content/loader.d.ts +1 -1
- package/dist/esm/content/loader.js +10 -6
- package/dist/esm/content/skill-policy.d.ts +11 -0
- package/dist/esm/content/skill-policy.js +33 -0
- 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/hooks/doctor.d.ts +11 -0
- package/dist/esm/hooks/doctor.js +114 -45
- package/dist/esm/hooks/pretool-guard/dev-routing.js +68 -5
- 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/pretool-guard/validators/worktree-discipline.js +62 -3
- package/dist/esm/hooks/status/index.js +1 -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/registration.js +1 -1
- 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/worktree.js +65 -3
- package/dist/esm/mcp/tools/wp-ui-compose.js +38 -4
- package/dist/esm/mcp/tools/wp-ui-preview.js +14 -7
- package/dist/esm/review/authority.js +56 -1
- package/dist/esm/review/delivery-verifier.d.ts +4 -0
- package/dist/esm/review/delivery-verifier.js +9 -1
- 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/symlinker/consumers.d.ts +3 -1
- package/dist/esm/symlinker/consumers.js +7 -1
- package/dist/esm/symlinker/unified-sync.d.ts +3 -3
- package/dist/esm/symlinker/unified-sync.js +88 -11
- package/dist/esm/test/shard-durations.json +0 -2
- 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 +16 -21
- package/dist/esm/cli/commands/init/scaffolders/subagents/index.d.ts +0 -6
- package/dist/esm/cli/commands/init/scaffolders/subagents/index.js +0 -77
- 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,86 @@
|
|
|
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
|
+
export declare const BLUEPRINT_EXEC_JOB_STATES: readonly ["preparing", "running", "terminal"];
|
|
14
|
+
export type BlueprintExecJobState = (typeof BLUEPRINT_EXEC_JOB_STATES)[number];
|
|
15
|
+
export type BlueprintExecJob = {
|
|
16
|
+
readonly jobId: string;
|
|
17
|
+
readonly slug: string;
|
|
18
|
+
readonly taskId: string | null;
|
|
19
|
+
/** Opaque single-use launch handle; never the composed argv or env. */
|
|
20
|
+
readonly launchHandle: string;
|
|
21
|
+
readonly sessionId: number | null;
|
|
22
|
+
readonly startedAt: number;
|
|
23
|
+
readonly state: BlueprintExecJobState;
|
|
24
|
+
};
|
|
25
|
+
export type RegisterBlueprintExecJobInput = {
|
|
26
|
+
readonly slug: string;
|
|
27
|
+
readonly taskId: string | null;
|
|
28
|
+
readonly launchHandle: string;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Bounded, insertion-ordered job store.
|
|
32
|
+
*
|
|
33
|
+
* Complexity: `register`, `bindSession`, `markTerminal`, and `get` are all
|
|
34
|
+
* Map-keyed O(1). Eviction is O(1) too — terminal job ids are appended to a
|
|
35
|
+
* FIFO queue as they terminate, so making room pops the front instead of
|
|
36
|
+
* rescanning the map. `list` is O(#jobs) over a capped map.
|
|
37
|
+
*/
|
|
38
|
+
/**
|
|
39
|
+
* Construction options. Shared by the class and {@link createBlueprintExecLedger}
|
|
40
|
+
* so the factory's public surface cannot drift away from what the constructor
|
|
41
|
+
* actually accepts.
|
|
42
|
+
*/
|
|
43
|
+
export type BlueprintExecLedgerOptions = {
|
|
44
|
+
readonly maxJobs?: number;
|
|
45
|
+
readonly staleUnboundMs?: number;
|
|
46
|
+
/** Injectable clock (tests). */
|
|
47
|
+
readonly now?: () => number;
|
|
48
|
+
};
|
|
49
|
+
export declare class BlueprintExecLedger {
|
|
50
|
+
private readonly byJobId;
|
|
51
|
+
/** FIFO of job ids that reached `terminal`, oldest first. */
|
|
52
|
+
private readonly terminalOrder;
|
|
53
|
+
private readonly maxJobs;
|
|
54
|
+
private readonly staleUnboundMs;
|
|
55
|
+
private readonly now;
|
|
56
|
+
constructor(options?: BlueprintExecLedgerOptions);
|
|
57
|
+
/** Evict the oldest terminal job. O(1) amortized via the FIFO queue. */
|
|
58
|
+
private evictOneTerminal;
|
|
59
|
+
/**
|
|
60
|
+
* Last-resort reclaim: the oldest job that never bound a session and whose
|
|
61
|
+
* launch handle can no longer be claimed.
|
|
62
|
+
*
|
|
63
|
+
* Without this, a daemon that only ever prepared launches which were never
|
|
64
|
+
* claimed (a crashing Rust peer) would wedge at the cap forever, because
|
|
65
|
+
* nothing would ever transition those jobs to terminal.
|
|
66
|
+
*
|
|
67
|
+
* O(#jobs) and only on the eviction path when the terminal queue is empty.
|
|
68
|
+
*/
|
|
69
|
+
private evictOneStaleUnbound;
|
|
70
|
+
register(input: RegisterBlueprintExecJobInput): {
|
|
71
|
+
readonly jobId: string;
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* Bind the spawned PTY session to a job. Rust-only caller.
|
|
75
|
+
*
|
|
76
|
+
* Single-assignment: a second bind for the same job is rejected so a
|
|
77
|
+
* compromised or buggy peer cannot re-point an existing job at another
|
|
78
|
+
* session.
|
|
79
|
+
*/
|
|
80
|
+
bindSession(jobId: string, sessionId: number): BlueprintExecJob;
|
|
81
|
+
markTerminal(jobId: string): BlueprintExecJob | null;
|
|
82
|
+
get(jobId: string): BlueprintExecJob | undefined;
|
|
83
|
+
list(): readonly BlueprintExecJob[];
|
|
84
|
+
size(): number;
|
|
85
|
+
}
|
|
86
|
+
export declare function createBlueprintExecLedger(options?: BlueprintExecLedgerOptions): BlueprintExecLedger;
|
|
@@ -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>>>;
|