sentinelayer-cli 0.46.5 → 0.48.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -3
- package/package.json +12 -7
- package/skills/omar-gate-setup/SKILL.md +103 -0
- package/skills/senti-agent-identity/SKILL.md +80 -0
- package/skills/senti-judge/SKILL.md +139 -0
- package/skills/senti-room-notifications/SKILL.md +73 -0
- package/skills/senti-room-notifications/listen-wrapper.cjs +150 -0
- package/skills/senti-room-notifications/package.json +1 -0
- package/skills/senti-room-notifications/test-classifier.js +109 -0
- package/skills/senti-room-notifications/test-receipt.js +54 -0
- package/skills/sentinelayer-cli/SKILL.md +222 -0
- package/src/agents/devtestbot/runner.js +22 -11
- package/src/agents/devtestbot/tool.js +24 -11
- package/src/agents/jules/fix-cycle.js +7 -1
- package/src/agents/jules/tools/runtime-audit.js +17 -12
- package/src/agents/shared-tools/dispatch-core.js +10 -5
- package/src/ai/aidenid.js +4 -2
- package/src/ai/domain-target-store.js +11 -5
- package/src/ai/identity-store.js +11 -5
- package/src/ai/site-store.js +8 -4
- package/src/audit/orchestrator.js +21 -14
- package/src/audit/package.js +6 -4
- package/src/audit/persona-loop.js +7 -0
- package/src/audit/replay.js +4 -2
- package/src/auth/gate.js +32 -1
- package/src/auth/session-store.js +37 -0
- package/src/board/evidence-resolvers.js +3 -4
- package/src/cli.js +23 -3
- package/src/commands/ai/identity-lifecycle.js +30 -26
- package/src/commands/ai/provision-governance.js +32 -24
- package/src/commands/ai/shared.js +6 -4
- package/src/commands/audit.js +14 -12
- package/src/commands/board.js +2 -2
- package/src/commands/chat.js +8 -6
- package/src/commands/judge.js +125 -0
- package/src/commands/legacy-args.js +1 -0
- package/src/commands/mcp.js +45 -52
- package/src/commands/omargate.js +2 -1
- package/src/commands/plugin.js +11 -3
- package/src/commands/review.js +9 -4
- package/src/commands/scan.js +7 -7
- package/src/commands/session.js +516 -30
- package/src/commands/skills.js +45 -0
- package/src/commands/watch.js +9 -7
- package/src/config/output-filesystem.js +156 -0
- package/src/config/schema.js +4 -2
- package/src/config/service.js +40 -4
- package/src/cost/history.js +15 -9
- package/src/daemon/artifact-lineage.js +15 -12
- package/src/daemon/assignment-ledger.js +31 -25
- package/src/daemon/ast-drift.js +7 -3
- package/src/daemon/budget-governor.js +26 -20
- package/src/daemon/error-worker.js +28 -22
- package/src/daemon/fix-cycle.js +5 -4
- package/src/daemon/hybrid-mapper.js +19 -16
- package/src/daemon/ingest-refresh.js +4 -0
- package/src/daemon/jira-lifecycle.js +21 -17
- package/src/daemon/operator-control.js +18 -15
- package/src/daemon/reliability-lane.js +18 -15
- package/src/daemon/scope-engine.js +12 -8
- package/src/daemon/watchdog.js +15 -12
- package/src/engram/build-state.js +24 -3
- package/src/engram/capability-index.js +367 -0
- package/src/engram/compile-graph.js +1063 -0
- package/src/engram/contracts/action-receipt.js +82 -0
- package/src/engram/contracts/capability-index.js +259 -0
- package/src/engram/contracts/capability.js +377 -0
- package/src/engram/contracts/content-hash.js +8 -0
- package/src/engram/contracts/evidence.js +258 -0
- package/src/engram/contracts/graph.js +594 -0
- package/src/engram/contracts/identity.js +8 -0
- package/src/engram/contracts/index.js +149 -0
- package/src/engram/contracts/media-asset.js +85 -0
- package/src/engram/contracts/money.js +249 -0
- package/src/engram/contracts/source-snapshot.js +57 -0
- package/src/engram/contracts/task-context-pack.js +114 -0
- package/src/engram/contracts/temporal-segment.js +72 -0
- package/src/engram/contracts/versions.js +96 -0
- package/src/engram/extract-html.js +692 -0
- package/src/engram/graph-store.js +877 -0
- package/src/engram/index.js +98 -14
- package/src/engram/ingest-site.js +132 -0
- package/src/engram/live-state.js +1180 -0
- package/src/engram/namespace.js +34 -1
- package/src/engram/retrieve-site.js +753 -0
- package/src/engram/safe-reader.js +597 -0
- package/src/engram/source-intake.js +251 -0
- package/src/engram/store.js +227 -9
- package/src/ingest/engine.js +21 -28
- package/src/judge/verdict.js +305 -0
- package/src/legacy-cli.js +96 -19
- package/src/mcp/bridge-inputs.js +25 -4
- package/src/mcp/cli-command-tools.js +70 -0
- package/src/mcp/registry.js +17 -13
- package/src/mcp/session-stdio-server.js +297 -19
- package/src/memory/blackboard.js +5 -4
- package/src/net/gh-api-path.js +35 -9
- package/src/net/test-egress-guard.js +16 -4
- package/src/plugin/manifest.js +12 -8
- package/src/review/ai-review.js +110 -10
- package/src/review/investor-dd-devtestbot.js +17 -6
- package/src/review/investor-dd-orchestrator.js +21 -13
- package/src/review/local-review.js +26 -16
- package/src/review/omargate-cache.js +5 -5
- package/src/review/omargate-orchestrator.js +89 -17
- package/src/review/replay.js +9 -4
- package/src/review/report.js +22 -13
- package/src/review/typesafe-severity.js +201 -0
- package/src/session/admission-auth.js +34 -7
- package/src/session/admission.js +110 -15
- package/src/session/attachment-summary.js +77 -0
- package/src/session/edit-summary.js +19 -0
- package/src/session/invitations.js +65 -1
- package/src/session/mentions.js +17 -9
- package/src/session/message-edits.js +5 -4
- package/src/session/preview.js +10 -2
- package/src/session/recap.js +3 -2
- package/src/session/room-files.js +519 -0
- package/src/session/route-classes.js +22 -8
- package/src/session/setup-guides.js +4 -0
- package/src/session/standup.js +192 -0
- package/src/session/sync.js +229 -10
- package/src/session/tickets.js +41 -0
- package/src/session/transcript.js +2 -1
- package/src/session/wake/resolve-target.js +7 -3
- package/src/skills/listener-runtime.cjs +92 -0
- package/src/skills/service.js +207 -0
- package/src/swarm/factory.js +5 -5
- package/src/swarm/pentest.js +9 -9
- package/src/swarm/report.js +4 -4
- package/src/swarm/runtime.js +15 -12
- package/src/swarm/scenario-dsl.js +7 -5
- package/src/telemetry/ledger.js +16 -7
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { installSkills, listSkills } from "../skills/service.js";
|
|
2
|
+
|
|
3
|
+
export function registerSkillsCommand(program) {
|
|
4
|
+
const skills = program.command("skills").description("Install and inspect bundled Senti agent skills");
|
|
5
|
+
for (const action of ["install", "list"]) {
|
|
6
|
+
const command = skills.command(action === "install" ? "install [names...]" : action)
|
|
7
|
+
.description(action === "install"
|
|
8
|
+
? "Install bundled skills (all, or only the named ones) without replacing local edits"
|
|
9
|
+
: "List bundled and installed skill versions and SHA-256 hashes")
|
|
10
|
+
.option("--dir <path>", "Skills directory (default: ~/.claude/skills)")
|
|
11
|
+
.option("--json", "Emit machine-readable output");
|
|
12
|
+
if (action === "install") command.option("--force", "Replace differing local files");
|
|
13
|
+
command.action(async (...args) => {
|
|
14
|
+
const invoked = args.at(-1);
|
|
15
|
+
const options = args.at(-2);
|
|
16
|
+
const names = action === "install" ? args[0] : undefined;
|
|
17
|
+
const json = Boolean(invoked.optsWithGlobals().json);
|
|
18
|
+
try {
|
|
19
|
+
const result = await (action === "install" ? installSkills({ ...options, names }) : listSkills(options));
|
|
20
|
+
if (json) {
|
|
21
|
+
console.log(JSON.stringify({ command: `skills ${action}`, ...result }, null, 2));
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
console.log(`Skills directory: ${result.directory}`);
|
|
25
|
+
for (const skill of result.skills) {
|
|
26
|
+
if (action === "install") {
|
|
27
|
+
console.log(`${skill.name} ${skill.version}: ${skill.written} written, ${skill.unchanged} unchanged (sha256 ${skill.hash})`);
|
|
28
|
+
} else {
|
|
29
|
+
console.log(`${skill.name}: ${skill.status}`);
|
|
30
|
+
console.log(` bundled: ${skill.bundled.version} sha256 ${skill.bundled.hash}`);
|
|
31
|
+
console.log(skill.installed ? ` installed: ${skill.installed.version || "unknown"} sha256 ${skill.installed.hash}` : " installed: none");
|
|
32
|
+
if (skill.error) console.log(` error: ${skill.error}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
} catch (error) {
|
|
36
|
+
if (json) console.log(JSON.stringify({ command: `skills ${action}`, error: {
|
|
37
|
+
code: error.code || "SKILLS_ERROR", message: error.message,
|
|
38
|
+
...(error.files ? { files: error.files } : {}),
|
|
39
|
+
} }, null, 2));
|
|
40
|
+
else console.error(error.message);
|
|
41
|
+
process.exitCode = 1;
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
package/src/commands/watch.js
CHANGED
|
@@ -11,7 +11,8 @@ import {
|
|
|
11
11
|
listRuntimeRunEvents,
|
|
12
12
|
resolveActiveAuthSession,
|
|
13
13
|
} from "../auth/service.js";
|
|
14
|
-
import { resolveOutputRoot } from "../config/service.js";
|
|
14
|
+
import { resolveOutputRoot, resolveOutputContext } from "../config/service.js";
|
|
15
|
+
import { bindOutputFileSystem, requireOutputFileSystem } from "../config/output-filesystem.js";
|
|
15
16
|
import { authLoginHint } from "../ui/command-hints.js";
|
|
16
17
|
import { isAuthenticated } from "../auth/credential-destinations.js";
|
|
17
18
|
|
|
@@ -254,11 +255,12 @@ async function resolveWatchArtifacts({
|
|
|
254
255
|
if (!enabled) {
|
|
255
256
|
return null;
|
|
256
257
|
}
|
|
257
|
-
const outputRoot = await
|
|
258
|
+
const { outputRoot, outputFs } = await resolveOutputContext({
|
|
258
259
|
cwd: targetPath,
|
|
259
260
|
outputDirOverride: outputDir,
|
|
260
261
|
env: process.env,
|
|
261
262
|
});
|
|
263
|
+
requireOutputFileSystem(outputFs);
|
|
262
264
|
const watchStamp = stableTimestampForFile();
|
|
263
265
|
const watchDir = path.join(
|
|
264
266
|
outputRoot,
|
|
@@ -266,12 +268,12 @@ async function resolveWatchArtifacts({
|
|
|
266
268
|
"runtime-watch",
|
|
267
269
|
sanitizePathSegment(runId)
|
|
268
270
|
);
|
|
269
|
-
await
|
|
270
|
-
return {
|
|
271
|
+
await outputFs.mkdir(watchDir, { recursive: true });
|
|
272
|
+
return bindOutputFileSystem({
|
|
271
273
|
watchDir,
|
|
272
274
|
eventsPath: path.join(watchDir, `events-${watchStamp}.ndjson`),
|
|
273
275
|
summaryPath: path.join(watchDir, `summary-${watchStamp}.json`),
|
|
274
|
-
};
|
|
276
|
+
}, outputFs);
|
|
275
277
|
}
|
|
276
278
|
|
|
277
279
|
export function registerWatchCommand(program) {
|
|
@@ -411,7 +413,7 @@ export function registerWatchCommand(program) {
|
|
|
411
413
|
eventCount += 1;
|
|
412
414
|
|
|
413
415
|
if (artifactPaths?.eventsPath) {
|
|
414
|
-
await
|
|
416
|
+
await artifactPaths.outputFs.appendFile(artifactPaths.eventsPath, `${JSON.stringify(event)}\n`, "utf-8");
|
|
415
417
|
}
|
|
416
418
|
|
|
417
419
|
if (emitJson) {
|
|
@@ -478,7 +480,7 @@ export function registerWatchCommand(program) {
|
|
|
478
480
|
};
|
|
479
481
|
|
|
480
482
|
if (artifactPaths?.summaryPath) {
|
|
481
|
-
await
|
|
483
|
+
await artifactPaths.outputFs.writeFile(artifactPaths.summaryPath, `${JSON.stringify(summary, null, 2)}\n`, "utf-8");
|
|
482
484
|
}
|
|
483
485
|
|
|
484
486
|
if (emitJson) {
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import fsp from "node:fs/promises";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
const outputCapabilities = new WeakSet();
|
|
7
|
+
|
|
8
|
+
/** Writers accept only a capability created for this operation's output policy. */
|
|
9
|
+
export function requireOutputFileSystem(outputFs) {
|
|
10
|
+
if (!outputCapabilities.has(outputFs)) {
|
|
11
|
+
throw new TypeError("An output filesystem capability is required for artifact writes.");
|
|
12
|
+
}
|
|
13
|
+
return outputFs;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function isInside(root, candidate) {
|
|
17
|
+
const relative = path.relative(root, candidate);
|
|
18
|
+
return relative === "" || (relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Validate the complete destination, including an existing final-file link. */
|
|
22
|
+
export async function assertWorkspaceOutputPath(destination, workspaceRoot) {
|
|
23
|
+
try {
|
|
24
|
+
const value = destination instanceof URL ? fileURLToPath(destination) : destination;
|
|
25
|
+
if (typeof value !== "string") throw new Error("Unverifiable artifact destination");
|
|
26
|
+
const root = path.resolve(workspaceRoot);
|
|
27
|
+
const candidate = path.resolve(value);
|
|
28
|
+
if (!isInside(root, candidate)) throw new Error("Artifact outside workspace");
|
|
29
|
+
const realRoot = await fsp.realpath(root);
|
|
30
|
+
let ancestor = candidate;
|
|
31
|
+
for (;;) {
|
|
32
|
+
let resolved;
|
|
33
|
+
try {
|
|
34
|
+
resolved = await fsp.realpath(ancestor);
|
|
35
|
+
} catch (error) {
|
|
36
|
+
if (error.code !== "ENOENT") throw error;
|
|
37
|
+
const entry = await fsp.lstat(ancestor).catch((statError) => {
|
|
38
|
+
if (statError.code !== "ENOENT") throw statError;
|
|
39
|
+
return null;
|
|
40
|
+
});
|
|
41
|
+
if (entry) {
|
|
42
|
+
if (entry.isSymbolicLink()) throw new Error("Unresolved artifact link");
|
|
43
|
+
// A concurrent artifact writer may have created this ordinary entry
|
|
44
|
+
// after realpath saw ENOENT. Resolve it once, then apply the same real
|
|
45
|
+
// workspace check below; never treat its mere existence as authority.
|
|
46
|
+
resolved = await fsp.realpath(ancestor);
|
|
47
|
+
} else {
|
|
48
|
+
const parent = path.dirname(ancestor);
|
|
49
|
+
if (parent === ancestor) throw error;
|
|
50
|
+
ancestor = parent;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (!isInside(realRoot, path.resolve(resolved, path.relative(ancestor, candidate)))) {
|
|
55
|
+
throw new Error("Artifact link leaves workspace");
|
|
56
|
+
}
|
|
57
|
+
return candidate;
|
|
58
|
+
}
|
|
59
|
+
} catch {
|
|
60
|
+
throw new Error("Workspace artifact destination must resolve inside the workspace.");
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Per-operation capability; never stored in a process-global path registry. */
|
|
65
|
+
export function createOutputFileSystem({ workspaceRoot = null } = {}) {
|
|
66
|
+
workspaceRoot = workspaceRoot ? path.resolve(workspaceRoot) : null;
|
|
67
|
+
const assertPath = async (destination) => {
|
|
68
|
+
if (workspaceRoot) return assertWorkspaceOutputPath(destination, workspaceRoot);
|
|
69
|
+
return destination;
|
|
70
|
+
};
|
|
71
|
+
const assertFilePath = async (destination) => {
|
|
72
|
+
const checked = await assertPath(destination);
|
|
73
|
+
if (workspaceRoot) {
|
|
74
|
+
const entry = await fsp.lstat(checked).catch((error) => {
|
|
75
|
+
if (error.code !== "ENOENT") throw new Error("Workspace artifact destination must resolve inside the workspace.");
|
|
76
|
+
return null;
|
|
77
|
+
});
|
|
78
|
+
if (entry?.isSymbolicLink() || (entry?.isFile() && entry.nlink > 1)) {
|
|
79
|
+
throw new Error("Workspace artifact destination must resolve inside the workspace without linked files.");
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return checked;
|
|
83
|
+
};
|
|
84
|
+
// Expose only read operations and the mutations guarded below. Adding a new
|
|
85
|
+
// writer must not silently inherit an unchecked fs/promises method.
|
|
86
|
+
const io = { assertPath, assertFilePath };
|
|
87
|
+
for (const method of ["access", "readFile", "readdir", "readlink", "realpath", "stat", "lstat", "statfs", "opendir"]) {
|
|
88
|
+
io[method] = fsp[method];
|
|
89
|
+
}
|
|
90
|
+
for (const method of ["mkdir", "rm", "unlink", "truncate"]) {
|
|
91
|
+
io[method] = async (destination, ...args) => {
|
|
92
|
+
const checked = await (method === "mkdir" ? assertPath(destination) : assertFilePath(destination));
|
|
93
|
+
return fsp[method](checked, ...args);
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
for (const method of ["rename", "copyFile"]) {
|
|
97
|
+
io[method] = async (source, destination, ...args) => {
|
|
98
|
+
const checkedSource = method === "rename" ? await assertFilePath(source) : source;
|
|
99
|
+
const checkedDestination = await assertFilePath(destination);
|
|
100
|
+
return fsp[method](checkedSource, checkedDestination, ...args);
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
io.open = async (destination, flags, ...args) => {
|
|
104
|
+
const writeFlags = constants.O_WRONLY | constants.O_RDWR | constants.O_CREAT | constants.O_APPEND | constants.O_TRUNC;
|
|
105
|
+
if (typeof flags === "number" ? Boolean(flags & writeFlags) : /[wa+]/.test(flags || "r")) {
|
|
106
|
+
const checked = await assertFilePath(destination);
|
|
107
|
+
return fsp.open(checked, workspaceRoot ? numericOpenFlags(flags) | (constants.O_NOFOLLOW || 0) : flags, ...args);
|
|
108
|
+
}
|
|
109
|
+
return fsp.open(destination, flags, ...args);
|
|
110
|
+
};
|
|
111
|
+
for (const method of ["writeFile", "appendFile"]) {
|
|
112
|
+
io[method] = async (destination, data, options) => {
|
|
113
|
+
if (!workspaceRoot) return fsp[method](destination, data, options);
|
|
114
|
+
if (options !== undefined && options !== null && typeof options !== "string" && typeof options !== "object") {
|
|
115
|
+
throw new TypeError("Artifact file options must be an encoding string or an object.");
|
|
116
|
+
}
|
|
117
|
+
const normalizedOptions = typeof options === "string" ? { encoding: options } : options || {};
|
|
118
|
+
const flag = normalizedOptions.flag || (method === "appendFile" ? "a" : "w");
|
|
119
|
+
const checked = await assertFilePath(destination);
|
|
120
|
+
return fsp[method](checked, data, {
|
|
121
|
+
...normalizedOptions,
|
|
122
|
+
flag: numericOpenFlags(flag) | (constants.O_NOFOLLOW || 0),
|
|
123
|
+
});
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
outputCapabilities.add(io);
|
|
127
|
+
return Object.freeze(io);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function numericOpenFlags(flags) {
|
|
131
|
+
if (typeof flags === "number") return flags;
|
|
132
|
+
const { O_RDONLY, O_WRONLY, O_RDWR, O_CREAT, O_TRUNC, O_APPEND, O_EXCL, O_SYNC } = constants;
|
|
133
|
+
const modes = {
|
|
134
|
+
r: O_RDONLY, rs: O_RDONLY | O_SYNC, sr: O_RDONLY | O_SYNC,
|
|
135
|
+
"r+": O_RDWR, "rs+": O_RDWR | O_SYNC, "sr+": O_RDWR | O_SYNC,
|
|
136
|
+
w: O_TRUNC | O_CREAT | O_WRONLY, wx: O_TRUNC | O_CREAT | O_WRONLY | O_EXCL,
|
|
137
|
+
xw: O_TRUNC | O_CREAT | O_WRONLY | O_EXCL,
|
|
138
|
+
"w+": O_TRUNC | O_CREAT | O_RDWR, "wx+": O_TRUNC | O_CREAT | O_RDWR | O_EXCL,
|
|
139
|
+
"xw+": O_TRUNC | O_CREAT | O_RDWR | O_EXCL,
|
|
140
|
+
a: O_APPEND | O_CREAT | O_WRONLY, ax: O_APPEND | O_CREAT | O_WRONLY | O_EXCL,
|
|
141
|
+
xa: O_APPEND | O_CREAT | O_WRONLY | O_EXCL,
|
|
142
|
+
"a+": O_APPEND | O_CREAT | O_RDWR, "ax+": O_APPEND | O_CREAT | O_RDWR | O_EXCL,
|
|
143
|
+
"xa+": O_APPEND | O_CREAT | O_RDWR | O_EXCL,
|
|
144
|
+
as: O_APPEND | O_CREAT | O_WRONLY | O_SYNC, sa: O_APPEND | O_CREAT | O_WRONLY | O_SYNC,
|
|
145
|
+
"as+": O_APPEND | O_CREAT | O_RDWR | O_SYNC, "sa+": O_APPEND | O_CREAT | O_RDWR | O_SYNC,
|
|
146
|
+
};
|
|
147
|
+
if (!Object.hasOwn(modes, flags)) throw new TypeError("Unsupported artifact file open flag.");
|
|
148
|
+
return modes[flags];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Storage objects are often returned as JSON. Keep the executable capability
|
|
152
|
+
// out of that public data shape while allowing explicit downstream propagation.
|
|
153
|
+
export function bindOutputFileSystem(storage, outputFs) {
|
|
154
|
+
Object.defineProperty(storage, "outputFs", { value: requireOutputFileSystem(outputFs), enumerable: false });
|
|
155
|
+
return storage;
|
|
156
|
+
}
|
package/src/config/schema.js
CHANGED
|
@@ -8,8 +8,10 @@ function emptyToUndefined(value) {
|
|
|
8
8
|
return normalized ? normalized : undefined;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
// optional() sits inside preprocess: zod >=4.2 validates the preprocessed value, so a blank value
|
|
12
|
+
// mapped to undefined must be accepted by the inner schema, not only by an outer optional().
|
|
13
|
+
const optionalTrimmedString = z.preprocess(emptyToUndefined, z.string().min(1).optional());
|
|
14
|
+
const optionalUrl = z.preprocess(emptyToUndefined, z.string().url().optional());
|
|
13
15
|
const optionalStringArray = z
|
|
14
16
|
.array(z.preprocess(emptyToUndefined, z.string().min(1)))
|
|
15
17
|
.optional();
|
package/src/config/service.js
CHANGED
|
@@ -4,6 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import { getConfigPaths } from "./paths.js";
|
|
5
5
|
import { ensureConfigFile, readConfigFile, writeConfigFile } from "./io.js";
|
|
6
6
|
import { CONFIG_KEYS, configSchema } from "./schema.js";
|
|
7
|
+
import { assertWorkspaceOutputPath, createOutputFileSystem } from "./output-filesystem.js";
|
|
7
8
|
|
|
8
9
|
const ENV_MAPPING = Object.freeze({
|
|
9
10
|
SENTINELAYER_API_URL: "apiUrl",
|
|
@@ -200,7 +201,38 @@ export function listConfigKeys() {
|
|
|
200
201
|
return [...CONFIG_KEYS];
|
|
201
202
|
}
|
|
202
203
|
|
|
203
|
-
|
|
204
|
+
function isInsideRoot(root, candidate) {
|
|
205
|
+
const relative = path.relative(root, candidate);
|
|
206
|
+
return relative === "" || (relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Every workspace-local output is confined, regardless of where its setting came
|
|
210
|
+
// from. Only an explicit absolute trusted-layer path outside the workspace opts out.
|
|
211
|
+
async function resolveOutputSelection(value, cwd, { project = false } = {}) {
|
|
212
|
+
try {
|
|
213
|
+
const root = path.resolve(cwd);
|
|
214
|
+
const absolute = path.isAbsolute(value);
|
|
215
|
+
const foreignAbsolute = path.win32.isAbsolute(value) || /^[A-Za-z]:/.test(value);
|
|
216
|
+
if ((project && (absolute || foreignAbsolute)) || (!absolute && foreignAbsolute)) throw new Error();
|
|
217
|
+
const candidate = path.resolve(root, value);
|
|
218
|
+
if (absolute && !isInsideRoot(root, candidate)) {
|
|
219
|
+
return { outputRoot: candidate, outputFs: createOutputFileSystem() };
|
|
220
|
+
}
|
|
221
|
+
// Backslash escapes remain rejected on POSIX as well, so config is portable.
|
|
222
|
+
if (!isInsideRoot(root, candidate) || !isInsideRoot(root, path.resolve(root, value.replace(/\\/g, "/")))) {
|
|
223
|
+
throw new Error();
|
|
224
|
+
}
|
|
225
|
+
return {
|
|
226
|
+
outputRoot: await assertWorkspaceOutputPath(candidate, root),
|
|
227
|
+
outputFs: createOutputFileSystem({ workspaceRoot: root }),
|
|
228
|
+
};
|
|
229
|
+
} catch {
|
|
230
|
+
// Do not echo private configuration values or filesystem errors containing their paths.
|
|
231
|
+
throw new Error("Workspace outputDir must resolve to a relative path inside the workspace.");
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export async function resolveOutputContext({
|
|
204
236
|
cwd = process.cwd(),
|
|
205
237
|
outputDirOverride = "",
|
|
206
238
|
env = process.env,
|
|
@@ -208,14 +240,18 @@ export async function resolveOutputRoot({
|
|
|
208
240
|
} = {}) {
|
|
209
241
|
const overrideValue = String(outputDirOverride || "").trim();
|
|
210
242
|
if (overrideValue) {
|
|
211
|
-
return
|
|
243
|
+
return resolveOutputSelection(overrideValue, cwd);
|
|
212
244
|
}
|
|
213
245
|
|
|
214
246
|
const config = await loadConfig({ cwd, env, homeDir });
|
|
215
247
|
const configuredOutputDir = String(config.resolved.outputDir || "").trim();
|
|
216
248
|
if (configuredOutputDir) {
|
|
217
|
-
return
|
|
249
|
+
return resolveOutputSelection(configuredOutputDir, cwd, { project: findConfigSource(config, "outputDir") === "project" });
|
|
218
250
|
}
|
|
219
251
|
|
|
220
|
-
return
|
|
252
|
+
return resolveOutputSelection(".sentinelayer", cwd);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export async function resolveOutputRoot(options = {}) {
|
|
256
|
+
return (await resolveOutputContext(options)).outputRoot;
|
|
221
257
|
}
|
package/src/cost/history.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { requireOutputFileSystem } from "../config/output-filesystem.js";
|
|
1
2
|
import fsp from "node:fs/promises";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { randomUUID } from "node:crypto";
|
|
4
5
|
|
|
5
|
-
import {
|
|
6
|
+
import { resolveOutputContext } from "../config/service.js";
|
|
6
7
|
import { rollupUsage } from "./tracker.js";
|
|
7
8
|
|
|
8
9
|
const HISTORY_VERSION = 1;
|
|
@@ -60,19 +61,23 @@ function normalizeEntry(entry) {
|
|
|
60
61
|
};
|
|
61
62
|
}
|
|
62
63
|
|
|
63
|
-
|
|
64
|
+
async function resolveCostHistoryOutput({
|
|
64
65
|
targetPath = ".",
|
|
65
66
|
outputDirOverride = "",
|
|
66
67
|
env,
|
|
67
68
|
homeDir,
|
|
68
69
|
} = {}) {
|
|
69
|
-
const outputRoot = await
|
|
70
|
+
const { outputRoot, outputFs } = await resolveOutputContext({
|
|
70
71
|
cwd: path.resolve(targetPath),
|
|
71
72
|
outputDirOverride,
|
|
72
73
|
env,
|
|
73
74
|
homeDir,
|
|
74
75
|
});
|
|
75
|
-
return path.join(outputRoot, HISTORY_FILE_NAME);
|
|
76
|
+
return { filePath: path.join(outputRoot, HISTORY_FILE_NAME), outputFs };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function resolveCostHistoryPath(options = {}) {
|
|
80
|
+
return (await resolveCostHistoryOutput(options)).filePath;
|
|
76
81
|
}
|
|
77
82
|
|
|
78
83
|
export async function loadCostHistory(options = {}) {
|
|
@@ -106,25 +111,26 @@ async function readCostHistoryFile(filePath) {
|
|
|
106
111
|
}
|
|
107
112
|
}
|
|
108
113
|
|
|
109
|
-
export async function saveCostHistory({ filePath, history }) {
|
|
114
|
+
export async function saveCostHistory({ filePath, history, outputFs }) {
|
|
115
|
+
requireOutputFileSystem(outputFs);
|
|
110
116
|
const payload = {
|
|
111
117
|
version: HISTORY_VERSION,
|
|
112
118
|
entries: Array.isArray(history?.entries) ? history.entries : [],
|
|
113
119
|
};
|
|
114
|
-
await
|
|
115
|
-
await
|
|
120
|
+
await outputFs.mkdir(path.dirname(filePath), { recursive: true });
|
|
121
|
+
await outputFs.writeFile(filePath, `${JSON.stringify(payload, null, 2)}\n`, "utf-8");
|
|
116
122
|
}
|
|
117
123
|
|
|
118
124
|
export async function appendCostEntry(options = {}, entry = {}) {
|
|
119
125
|
const normalizedEntry = normalizeEntry(entry);
|
|
120
|
-
const filePath = await
|
|
126
|
+
const { filePath, outputFs } = await resolveCostHistoryOutput(options);
|
|
121
127
|
return withCostHistoryWriteQueue(filePath, async () => {
|
|
122
128
|
const history = await readCostHistoryFile(filePath);
|
|
123
129
|
const nextHistory = {
|
|
124
130
|
version: HISTORY_VERSION,
|
|
125
131
|
entries: [...history.entries, normalizedEntry],
|
|
126
132
|
};
|
|
127
|
-
await saveCostHistory({ filePath, history: nextHistory });
|
|
133
|
+
await saveCostHistory({ filePath, history: nextHistory, outputFs });
|
|
128
134
|
return {
|
|
129
135
|
filePath,
|
|
130
136
|
entry: normalizedEntry,
|
|
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import fsp from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
|
|
5
|
+
import { bindOutputFileSystem, requireOutputFileSystem } from "../config/output-filesystem.js";
|
|
5
6
|
import { listAssignments, resolveAssignmentLedgerStorage } from "./assignment-ledger.js";
|
|
6
7
|
import { getBudgetHealthColor, resolveOperatorControlStorage } from "./operator-control.js";
|
|
7
8
|
import { listBudgetStates, resolveBudgetGovernorStorage } from "./budget-governor.js";
|
|
@@ -215,7 +216,7 @@ export async function writeCloseoutArtifact({
|
|
|
215
216
|
});
|
|
216
217
|
const dateKey = normalizeDateKey(date, normalizedNow);
|
|
217
218
|
const artifactDir = resolveWorkItemArtifactDir(storage, normalizedWorkItemId, dateKey);
|
|
218
|
-
await
|
|
219
|
+
await storage.outputFs.mkdir(artifactDir, { recursive: true });
|
|
219
220
|
const closeoutPath = path.join(artifactDir, "closeout.json");
|
|
220
221
|
|
|
221
222
|
const entries = await fsp.readdir(artifactDir, { withFileTypes: true }).catch((error) => {
|
|
@@ -273,7 +274,7 @@ export async function writeCloseoutArtifact({
|
|
|
273
274
|
),
|
|
274
275
|
anchorSha256,
|
|
275
276
|
};
|
|
276
|
-
await writeJsonFile(closeoutPath, payload);
|
|
277
|
+
await writeJsonFile(closeoutPath, payload, storage.outputFs);
|
|
277
278
|
return {
|
|
278
279
|
closeoutPath,
|
|
279
280
|
payload,
|
|
@@ -390,14 +391,16 @@ export async function verifyArtifactChain({
|
|
|
390
391
|
};
|
|
391
392
|
}
|
|
392
393
|
|
|
393
|
-
async function writeJsonFile(filePath, payload = {}) {
|
|
394
|
-
|
|
395
|
-
await
|
|
394
|
+
async function writeJsonFile(filePath, payload = {}, outputFs) {
|
|
395
|
+
requireOutputFileSystem(outputFs);
|
|
396
|
+
await outputFs.mkdir(path.dirname(filePath), { recursive: true });
|
|
397
|
+
await outputFs.writeFile(filePath, `${JSON.stringify(payload, null, 2)}\n`, "utf-8");
|
|
396
398
|
}
|
|
397
399
|
|
|
398
|
-
async function appendJsonLine(filePath, payload = {}) {
|
|
399
|
-
|
|
400
|
-
await
|
|
400
|
+
async function appendJsonLine(filePath, payload = {}, outputFs) {
|
|
401
|
+
requireOutputFileSystem(outputFs);
|
|
402
|
+
await outputFs.mkdir(path.dirname(filePath), { recursive: true });
|
|
403
|
+
await outputFs.appendFile(filePath, `${JSON.stringify(payload)}\n`, "utf-8");
|
|
401
404
|
}
|
|
402
405
|
|
|
403
406
|
async function readJsonFile(filePath, fallbackValue) {
|
|
@@ -518,12 +521,12 @@ export async function resolveArtifactLineageStorage({
|
|
|
518
521
|
homeDir,
|
|
519
522
|
});
|
|
520
523
|
const lineageDir = path.join(daemonStorage.baseDir, "lineage");
|
|
521
|
-
return {
|
|
524
|
+
return bindOutputFileSystem({
|
|
522
525
|
...daemonStorage,
|
|
523
526
|
lineageDir,
|
|
524
527
|
lineageIndexPath: path.join(lineageDir, "lineage-index.json"),
|
|
525
528
|
lineageEventsPath: path.join(lineageDir, "lineage-events.ndjson"),
|
|
526
|
-
};
|
|
529
|
+
}, daemonStorage.outputFs);
|
|
527
530
|
}
|
|
528
531
|
|
|
529
532
|
export async function buildArtifactLineageIndex({
|
|
@@ -799,7 +802,7 @@ export async function buildArtifactLineageIndex({
|
|
|
799
802
|
};
|
|
800
803
|
|
|
801
804
|
await Promise.all([
|
|
802
|
-
writeJsonFile(storage.lineageIndexPath, index),
|
|
805
|
+
writeJsonFile(storage.lineageIndexPath, index, storage.outputFs),
|
|
803
806
|
appendJsonLine(storage.lineageEventsPath, {
|
|
804
807
|
timestamp: normalizedNow,
|
|
805
808
|
eventType: "lineage_build",
|
|
@@ -809,7 +812,7 @@ export async function buildArtifactLineageIndex({
|
|
|
809
812
|
jiraLinkedCount,
|
|
810
813
|
budgetGuardedCount,
|
|
811
814
|
operatorCoveredCount,
|
|
812
|
-
}),
|
|
815
|
+
}, storage.outputFs),
|
|
813
816
|
]);
|
|
814
817
|
|
|
815
818
|
return {
|
|
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import fsp from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
|
|
5
|
+
import { bindOutputFileSystem, requireOutputFileSystem } from "../config/output-filesystem.js";
|
|
5
6
|
import { WORK_ITEM_STATUSES, resolveErrorDaemonStorage } from "./error-worker.js";
|
|
6
7
|
|
|
7
8
|
const LEDGER_SCHEMA_VERSION = "1.0.0";
|
|
@@ -170,14 +171,16 @@ async function loadJsonFile(filePath, defaultFactory) {
|
|
|
170
171
|
}
|
|
171
172
|
}
|
|
172
173
|
|
|
173
|
-
async function writeJsonFile(filePath, payload = {}) {
|
|
174
|
-
|
|
175
|
-
await
|
|
174
|
+
async function writeJsonFile(filePath, payload = {}, outputFs) {
|
|
175
|
+
requireOutputFileSystem(outputFs);
|
|
176
|
+
await outputFs.mkdir(path.dirname(filePath), { recursive: true });
|
|
177
|
+
await outputFs.writeFile(filePath, `${JSON.stringify(payload, null, 2)}\n`, "utf-8");
|
|
176
178
|
}
|
|
177
179
|
|
|
178
|
-
async function appendEvent(filePath, payload = {}) {
|
|
179
|
-
|
|
180
|
-
await
|
|
180
|
+
async function appendEvent(filePath, payload = {}, outputFs) {
|
|
181
|
+
requireOutputFileSystem(outputFs);
|
|
182
|
+
await outputFs.mkdir(path.dirname(filePath), { recursive: true });
|
|
183
|
+
await outputFs.appendFile(filePath, `${JSON.stringify(payload)}\n`, "utf-8");
|
|
181
184
|
}
|
|
182
185
|
|
|
183
186
|
async function loadLedger(ledgerPath, nowIso = new Date().toISOString()) {
|
|
@@ -193,7 +196,8 @@ async function loadLedger(ledgerPath, nowIso = new Date().toISOString()) {
|
|
|
193
196
|
};
|
|
194
197
|
}
|
|
195
198
|
|
|
196
|
-
async function writeLedger(ledgerPath, ledger = {}, nowIso = new Date().toISOString()) {
|
|
199
|
+
async function writeLedger(ledgerPath, ledger = {}, nowIso = new Date().toISOString(), outputFs) {
|
|
200
|
+
requireOutputFileSystem(outputFs);
|
|
197
201
|
const normalized = {
|
|
198
202
|
schemaVersion: LEDGER_SCHEMA_VERSION,
|
|
199
203
|
generatedAt: normalizeIsoTimestamp(nowIso, nowIso),
|
|
@@ -203,7 +207,7 @@ async function writeLedger(ledgerPath, ledger = {}, nowIso = new Date().toISOStr
|
|
|
203
207
|
.filter((record) => normalizeString(record.workItemId))
|
|
204
208
|
: [],
|
|
205
209
|
};
|
|
206
|
-
await writeJsonFile(ledgerPath, normalized);
|
|
210
|
+
await writeJsonFile(ledgerPath, normalized, outputFs);
|
|
207
211
|
return normalized;
|
|
208
212
|
}
|
|
209
213
|
|
|
@@ -218,7 +222,8 @@ async function loadQueue(queuePath, nowIso = new Date().toISOString()) {
|
|
|
218
222
|
};
|
|
219
223
|
}
|
|
220
224
|
|
|
221
|
-
async function writeQueue(queuePath, queue = {}, nowIso = new Date().toISOString()) {
|
|
225
|
+
async function writeQueue(queuePath, queue = {}, nowIso = new Date().toISOString(), outputFs) {
|
|
226
|
+
requireOutputFileSystem(outputFs);
|
|
222
227
|
const normalized = {
|
|
223
228
|
schemaVersion: QUEUE_SCHEMA_VERSION,
|
|
224
229
|
generatedAt: normalizeIsoTimestamp(nowIso, nowIso),
|
|
@@ -226,16 +231,17 @@ async function writeQueue(queuePath, queue = {}, nowIso = new Date().toISOString
|
|
|
226
231
|
? queue.items.map((item) => normalizeQueueItem(item, nowIso)).filter((item) => item.workItemId)
|
|
227
232
|
: [],
|
|
228
233
|
};
|
|
229
|
-
await writeJsonFile(queuePath, normalized);
|
|
234
|
+
await writeJsonFile(queuePath, normalized, outputFs);
|
|
230
235
|
return normalized;
|
|
231
236
|
}
|
|
232
237
|
|
|
233
238
|
function getAssignmentStoragePaths(baseStorage = {}) {
|
|
234
|
-
|
|
239
|
+
requireOutputFileSystem(baseStorage.outputFs);
|
|
240
|
+
return bindOutputFileSystem({
|
|
235
241
|
...baseStorage,
|
|
236
242
|
ledgerPath: path.join(baseStorage.baseDir, "assignment-ledger.json"),
|
|
237
243
|
eventsPath: path.join(baseStorage.baseDir, "assignment-events.ndjson"),
|
|
238
|
-
};
|
|
244
|
+
}, baseStorage.outputFs);
|
|
239
245
|
}
|
|
240
246
|
|
|
241
247
|
async function loadLedgerAndQueue(storage, nowIso) {
|
|
@@ -436,8 +442,8 @@ export async function claimAssignment({
|
|
|
436
442
|
});
|
|
437
443
|
|
|
438
444
|
const [savedLedger, savedQueue] = await Promise.all([
|
|
439
|
-
writeLedger(storage.ledgerPath, ledger, normalizedNow),
|
|
440
|
-
writeQueue(storage.queuePath, queue, normalizedNow),
|
|
445
|
+
writeLedger(storage.ledgerPath, ledger, normalizedNow, storage.outputFs),
|
|
446
|
+
writeQueue(storage.queuePath, queue, normalizedNow, storage.outputFs),
|
|
441
447
|
]);
|
|
442
448
|
|
|
443
449
|
await appendEvent(storage.eventsPath, {
|
|
@@ -451,7 +457,7 @@ export async function claimAssignment({
|
|
|
451
457
|
stage: normalizeString(stage) || "triage",
|
|
452
458
|
runId: normalizeString(runId) || null,
|
|
453
459
|
jiraIssueKey: normalizeString(jiraIssueKey) || null,
|
|
454
|
-
});
|
|
460
|
+
}, storage.outputFs);
|
|
455
461
|
|
|
456
462
|
return {
|
|
457
463
|
...storage,
|
|
@@ -537,8 +543,8 @@ export async function heartbeatAssignment({
|
|
|
537
543
|
});
|
|
538
544
|
|
|
539
545
|
const [savedLedger, savedQueue] = await Promise.all([
|
|
540
|
-
writeLedger(storage.ledgerPath, ledger, normalizedNow),
|
|
541
|
-
writeQueue(storage.queuePath, queue, normalizedNow),
|
|
546
|
+
writeLedger(storage.ledgerPath, ledger, normalizedNow, storage.outputFs),
|
|
547
|
+
writeQueue(storage.queuePath, queue, normalizedNow, storage.outputFs),
|
|
542
548
|
]);
|
|
543
549
|
|
|
544
550
|
await appendEvent(storage.eventsPath, {
|
|
@@ -549,7 +555,7 @@ export async function heartbeatAssignment({
|
|
|
549
555
|
agentIdentity: normalizedAgentIdentity,
|
|
550
556
|
stage: next.stage,
|
|
551
557
|
leaseExpiresAt,
|
|
552
|
-
});
|
|
558
|
+
}, storage.outputFs);
|
|
553
559
|
|
|
554
560
|
return {
|
|
555
561
|
...storage,
|
|
@@ -638,8 +644,8 @@ export async function releaseAssignment({
|
|
|
638
644
|
});
|
|
639
645
|
|
|
640
646
|
const [savedLedger, savedQueue] = await Promise.all([
|
|
641
|
-
writeLedger(storage.ledgerPath, ledger, normalizedNow),
|
|
642
|
-
writeQueue(storage.queuePath, queue, normalizedNow),
|
|
647
|
+
writeLedger(storage.ledgerPath, ledger, normalizedNow, storage.outputFs),
|
|
648
|
+
writeQueue(storage.queuePath, queue, normalizedNow, storage.outputFs),
|
|
643
649
|
]);
|
|
644
650
|
|
|
645
651
|
await appendEvent(storage.eventsPath, {
|
|
@@ -650,7 +656,7 @@ export async function releaseAssignment({
|
|
|
650
656
|
agentIdentity: next.assignedAgentIdentity,
|
|
651
657
|
status: normalizedStatus,
|
|
652
658
|
reason: next.releaseReason,
|
|
653
|
-
});
|
|
659
|
+
}, storage.outputFs);
|
|
654
660
|
|
|
655
661
|
return {
|
|
656
662
|
...storage,
|
|
@@ -746,8 +752,8 @@ export async function reassignAssignment({
|
|
|
746
752
|
});
|
|
747
753
|
|
|
748
754
|
const [savedLedger, savedQueue] = await Promise.all([
|
|
749
|
-
writeLedger(storage.ledgerPath, ledger, normalizedNow),
|
|
750
|
-
writeQueue(storage.queuePath, queue, normalizedNow),
|
|
755
|
+
writeLedger(storage.ledgerPath, ledger, normalizedNow, storage.outputFs),
|
|
756
|
+
writeQueue(storage.queuePath, queue, normalizedNow, storage.outputFs),
|
|
751
757
|
]);
|
|
752
758
|
|
|
753
759
|
await appendEvent(storage.eventsPath, {
|
|
@@ -760,7 +766,7 @@ export async function reassignAssignment({
|
|
|
760
766
|
leaseTtlSeconds: normalizedTtl,
|
|
761
767
|
leaseExpiresAt,
|
|
762
768
|
stage: next.stage,
|
|
763
|
-
});
|
|
769
|
+
}, storage.outputFs);
|
|
764
770
|
|
|
765
771
|
return {
|
|
766
772
|
...storage,
|
|
@@ -1073,7 +1079,7 @@ export async function ensureWorkItemQueued({
|
|
|
1073
1079
|
queue.items.push(normalizedItem);
|
|
1074
1080
|
}
|
|
1075
1081
|
|
|
1076
|
-
const savedQueue = await writeQueue(storage.queuePath, queue, normalizedNow);
|
|
1082
|
+
const savedQueue = await writeQueue(storage.queuePath, queue, normalizedNow, storage.outputFs);
|
|
1077
1083
|
const queueItem = savedQueue.items.find((item) => item.workItemId === normalizedWorkItemId) || normalizedItem;
|
|
1078
1084
|
return {
|
|
1079
1085
|
...storage,
|