@kylecheng3146/agent-ops 0.0.1

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.
Files changed (115) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +83 -0
  3. package/SECURITY.md +22 -0
  4. package/dist/packages/cli/src/args.js +322 -0
  5. package/dist/packages/cli/src/bin.js +290 -0
  6. package/dist/packages/cli/src/cli.js +80 -0
  7. package/dist/packages/cli/src/commands/config.js +8 -0
  8. package/dist/packages/cli/src/commands/doctor.js +32 -0
  9. package/dist/packages/cli/src/commands/index.js +16 -0
  10. package/dist/packages/cli/src/commands/init.js +65 -0
  11. package/dist/packages/cli/src/commands/review.js +71 -0
  12. package/dist/packages/cli/src/commands/task.js +141 -0
  13. package/dist/packages/cli/src/commands/trust.js +48 -0
  14. package/dist/packages/cli/src/commands/uninstall.js +58 -0
  15. package/dist/packages/cli/src/commands/update.js +58 -0
  16. package/dist/packages/cli/src/commands/verify.js +108 -0
  17. package/dist/packages/cli/src/output.js +53 -0
  18. package/dist/packages/cli/src/plan-output.js +28 -0
  19. package/dist/packages/cli/src/wizard.js +75 -0
  20. package/dist/runtime/src/adapters/claude/config.js +109 -0
  21. package/dist/runtime/src/adapters/claude/events.js +8 -0
  22. package/dist/runtime/src/adapters/claude/input.js +38 -0
  23. package/dist/runtime/src/adapters/claude/output.js +30 -0
  24. package/dist/runtime/src/adapters/codex/config.js +93 -0
  25. package/dist/runtime/src/adapters/codex/events.js +8 -0
  26. package/dist/runtime/src/adapters/codex/input.js +41 -0
  27. package/dist/runtime/src/adapters/codex/output.js +24 -0
  28. package/dist/runtime/src/config/explain.js +34 -0
  29. package/dist/runtime/src/config/load.js +25 -0
  30. package/dist/runtime/src/config/merge.js +170 -0
  31. package/dist/runtime/src/config/migrate.js +61 -0
  32. package/dist/runtime/src/contracts.js +1 -0
  33. package/dist/runtime/src/discovery/go.js +145 -0
  34. package/dist/runtime/src/discovery/index.js +40 -0
  35. package/dist/runtime/src/discovery/make.js +162 -0
  36. package/dist/runtime/src/discovery/node.js +175 -0
  37. package/dist/runtime/src/discovery/python.js +163 -0
  38. package/dist/runtime/src/discovery/rust.js +159 -0
  39. package/dist/runtime/src/discovery/types.js +1 -0
  40. package/dist/runtime/src/fs/hash.js +19 -0
  41. package/dist/runtime/src/fs/managed-block.js +90 -0
  42. package/dist/runtime/src/fs/manifest.js +24 -0
  43. package/dist/runtime/src/fs/mutation-worker.js +185 -0
  44. package/dist/runtime/src/fs/paths.js +96 -0
  45. package/dist/runtime/src/fs/transaction.js +498 -0
  46. package/dist/runtime/src/guardrails/destructive.js +207 -0
  47. package/dist/runtime/src/guardrails/evaluate.js +9 -0
  48. package/dist/runtime/src/guardrails/exceptions.js +49 -0
  49. package/dist/runtime/src/guardrails/secrets.js +97 -0
  50. package/dist/runtime/src/guardrails/types.js +9 -0
  51. package/dist/runtime/src/hooks/dispatch.js +78 -0
  52. package/dist/runtime/src/hooks/events.js +1 -0
  53. package/dist/runtime/src/hooks/hook-entry.js +19 -0
  54. package/dist/runtime/src/hooks/normalize.js +59 -0
  55. package/dist/runtime/src/hooks/output.js +12 -0
  56. package/dist/runtime/src/hooks/shell.js +138 -0
  57. package/dist/runtime/src/hooks/stop-verify.js +70 -0
  58. package/dist/runtime/src/install/apply.js +70 -0
  59. package/dist/runtime/src/install/doctor.js +196 -0
  60. package/dist/runtime/src/install/harness.js +87 -0
  61. package/dist/runtime/src/install/ownership.js +84 -0
  62. package/dist/runtime/src/install/plan.js +257 -0
  63. package/dist/runtime/src/install/profiles.js +28 -0
  64. package/dist/runtime/src/install/types.js +1 -0
  65. package/dist/runtime/src/install/uninstall.js +206 -0
  66. package/dist/runtime/src/install/update.js +123 -0
  67. package/dist/runtime/src/logging/local-log.js +158 -0
  68. package/dist/runtime/src/registry/npm.js +141 -0
  69. package/dist/runtime/src/review/claude-runner.js +4 -0
  70. package/dist/runtime/src/review/codex-runner.js +4 -0
  71. package/dist/runtime/src/review/packet.js +10 -0
  72. package/dist/runtime/src/review/result.js +24 -0
  73. package/dist/runtime/src/review/roles.js +3 -0
  74. package/dist/runtime/src/review/runner.js +45 -0
  75. package/dist/runtime/src/schema/validate.js +584 -0
  76. package/dist/runtime/src/security/permissions.js +654 -0
  77. package/dist/runtime/src/security/redact.js +41 -0
  78. package/dist/runtime/src/security/trust.js +209 -0
  79. package/dist/runtime/src/task/render.js +43 -0
  80. package/dist/runtime/src/task/service.js +235 -0
  81. package/dist/runtime/src/task/store.js +265 -0
  82. package/dist/runtime/src/verify/change-surface.js +86 -0
  83. package/dist/runtime/src/verify/evidence.js +89 -0
  84. package/dist/runtime/src/verify/fingerprint.js +67 -0
  85. package/dist/runtime/src/verify/scope.js +69 -0
  86. package/dist/runtime/src/verify/service.js +217 -0
  87. package/dist/runtime/src/verify/spawn.js +326 -0
  88. package/dist/runtime/src/verify/test-count.js +148 -0
  89. package/docs/en/spec/README.md +13 -0
  90. package/docs/en/spec/acceptance-and-evidence.md +21 -0
  91. package/docs/en/spec/delegation.md +21 -0
  92. package/docs/en/spec/guardrails.md +21 -0
  93. package/docs/en/spec/harness-adapters.md +21 -0
  94. package/docs/en/spec/judgment.md +21 -0
  95. package/docs/en/spec/loop-engineering.md +23 -0
  96. package/docs/en/spec/maintenance.md +21 -0
  97. package/docs/en/spec/review.md +21 -0
  98. package/docs/en/spec/troubleshooting.md +21 -0
  99. package/docs/zh-TW/spec/README.md +13 -0
  100. package/docs/zh-TW/spec/acceptance-and-evidence.md +23 -0
  101. package/docs/zh-TW/spec/delegation.md +23 -0
  102. package/docs/zh-TW/spec/guardrails.md +23 -0
  103. package/docs/zh-TW/spec/harness-adapters.md +23 -0
  104. package/docs/zh-TW/spec/judgment.md +23 -0
  105. package/docs/zh-TW/spec/loop-engineering.md +23 -0
  106. package/docs/zh-TW/spec/maintenance.md +23 -0
  107. package/docs/zh-TW/spec/review.md +23 -0
  108. package/docs/zh-TW/spec/troubleshooting.md +23 -0
  109. package/package.json +41 -0
  110. package/schemas/config.schema.json +231 -0
  111. package/schemas/evidence.schema.json +115 -0
  112. package/schemas/manifest.schema.json +116 -0
  113. package/schemas/task.schema.json +59 -0
  114. package/templates/common/AGENTS.block.md +3 -0
  115. package/templates/common/CLAUDE.block.md +3 -0
@@ -0,0 +1,19 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFile } from "node:fs/promises";
3
+ export function sha256(value) {
4
+ return createHash("sha256").update(value).digest("hex");
5
+ }
6
+ export async function hashFile(path) {
7
+ try {
8
+ return sha256(await readFile(path));
9
+ }
10
+ catch (error) {
11
+ if (typeof error === "object" &&
12
+ error !== null &&
13
+ "code" in error &&
14
+ error.code === "ENOENT") {
15
+ return null;
16
+ }
17
+ throw error;
18
+ }
19
+ }
@@ -0,0 +1,90 @@
1
+ import { AgentOpsError } from "./paths.js";
2
+ function escapeRegExp(value) {
3
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4
+ }
5
+ function assertBlockId(id) {
6
+ if (!/^[a-z][a-z0-9-]{0,127}$/.test(id)) {
7
+ throw new AgentOpsError("INVALID_BLOCK_ID", `Invalid block ID: ${id}`);
8
+ }
9
+ }
10
+ function assertExactMarkerSyntax(source) {
11
+ const validMarker = /^<!-- agent-ops:(?:start [a-z][a-z0-9-]{0,127} v[1-9][0-9]*|end [a-z][a-z0-9-]{0,127}) -->$/;
12
+ for (const line of source.split(/\r?\n/)) {
13
+ if (/<!--\s*agent-ops:/.test(line) && !validMarker.test(line)) {
14
+ throw new AgentOpsError("MALFORMED_MANAGED_BLOCK", "Managed block markers must use the exact agent-ops marker syntax.");
15
+ }
16
+ }
17
+ }
18
+ export function managedBlockMarkers(id, version) {
19
+ assertBlockId(id);
20
+ if (!Number.isSafeInteger(version) || version < 1) {
21
+ throw new AgentOpsError("INVALID_BLOCK_VERSION", `Invalid block version: ${version}`);
22
+ }
23
+ return {
24
+ start: `<!-- agent-ops:start ${id} v${version} -->`,
25
+ end: `<!-- agent-ops:end ${id} -->`
26
+ };
27
+ }
28
+ function locateMarkers(source, id, version) {
29
+ assertBlockId(id);
30
+ assertExactMarkerSyntax(source);
31
+ const escapedId = escapeRegExp(id);
32
+ const startMatches = [
33
+ ...source.matchAll(new RegExp(`<!-- agent-ops:start ${escapedId} v[0-9]+ -->`, "g"))
34
+ ];
35
+ const end = `<!-- agent-ops:end ${id} -->`;
36
+ const endMatches = [...source.matchAll(new RegExp(escapeRegExp(end), "g"))];
37
+ if (startMatches.length === 0 && endMatches.length === 0) {
38
+ return null;
39
+ }
40
+ const expectedStart = version === undefined ? startMatches[0]?.[0] : managedBlockMarkers(id, version).start;
41
+ const start = startMatches[0]?.[0];
42
+ const startIndex = startMatches[0]?.index;
43
+ const endIndex = endMatches[0]?.index;
44
+ if (startMatches.length !== 1 ||
45
+ endMatches.length !== 1 ||
46
+ start === undefined ||
47
+ expectedStart === undefined ||
48
+ start !== expectedStart ||
49
+ startIndex === undefined ||
50
+ endIndex === undefined ||
51
+ startIndex >= endIndex) {
52
+ throw new AgentOpsError("MALFORMED_MANAGED_BLOCK", `Managed block markers are missing, duplicated, reversed, or version-mismatched: ${id}`);
53
+ }
54
+ return { start, end, startIndex, endIndex };
55
+ }
56
+ function renderBlock(options) {
57
+ if (/<!--\s*agent-ops:/.test(options.content)) {
58
+ throw new AgentOpsError("AMBIGUOUS_MANAGED_CONTENT", "Managed content must not contain agent-ops marker boundaries.");
59
+ }
60
+ const { start, end } = managedBlockMarkers(options.id, options.version);
61
+ const content = options.content.replace(/\r\n/g, "\n").replace(/\n+$/g, "");
62
+ return `${start}\n${content}\n${end}`;
63
+ }
64
+ export function applyManagedBlock(source, options) {
65
+ const block = renderBlock(options);
66
+ const located = locateMarkers(source, options.id, options.version);
67
+ if (located === null) {
68
+ if (source.length === 0) {
69
+ return `${block}\n`;
70
+ }
71
+ return `${source.replace(/\n*$/, "\n\n")}${block}\n`;
72
+ }
73
+ return `${source.slice(0, located.startIndex)}${block}${source.slice(located.endIndex + located.end.length)}`;
74
+ }
75
+ export function removeManagedBlock(source, id) {
76
+ const located = locateMarkers(source, id);
77
+ if (located === null) {
78
+ return source;
79
+ }
80
+ let before = source.slice(0, located.startIndex);
81
+ let after = source.slice(located.endIndex + located.end.length);
82
+ if (before.length === 0 && after === "\n") {
83
+ return "";
84
+ }
85
+ if (before.endsWith("\n\n") && after.startsWith("\n")) {
86
+ before = before.slice(0, -1);
87
+ after = after.slice(1);
88
+ }
89
+ return `${before}${after}`;
90
+ }
@@ -0,0 +1,24 @@
1
+ import { validateManifest } from "../schema/validate.js";
2
+ import { AgentOpsError } from "./paths.js";
3
+ export const PROJECT_MANIFEST_PATH = ".agent-ops/manifest.json";
4
+ export function parseInstallManifest(source) {
5
+ let value;
6
+ try {
7
+ value = JSON.parse(source);
8
+ }
9
+ catch (error) {
10
+ throw new AgentOpsError("MANIFEST_INVALID_JSON", "The installation manifest is not valid JSON.", { cause: error });
11
+ }
12
+ const result = validateManifest(value);
13
+ if (!result.ok) {
14
+ throw new AgentOpsError("MANIFEST_INVALID", `${result.errors[0]?.path ?? "$"}: ${result.errors[0]?.message ?? "Invalid installation manifest."}`);
15
+ }
16
+ return result.value;
17
+ }
18
+ export function formatInstallManifest(manifest) {
19
+ const result = validateManifest(manifest);
20
+ if (!result.ok) {
21
+ throw new AgentOpsError("MANIFEST_INVALID", `${result.errors[0]?.path ?? "$"}: ${result.errors[0]?.message ?? "Invalid installation manifest."}`);
22
+ }
23
+ return `${JSON.stringify(result.value, null, 2)}\n`;
24
+ }
@@ -0,0 +1,185 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { chmod, lstat, mkdir, open, readFile, realpath, rename, rmdir, rm, stat, unlink } from "node:fs/promises";
3
+ import { sha256 } from "./hash.js";
4
+ class MutationWorkerError extends Error {
5
+ code;
6
+ constructor(code, message) {
7
+ super(message);
8
+ this.code = code;
9
+ }
10
+ }
11
+ function isMissing(error) {
12
+ return (typeof error === "object" &&
13
+ error !== null &&
14
+ "code" in error &&
15
+ error.code === "ENOENT");
16
+ }
17
+ function parseRequest(value) {
18
+ if (value === undefined) {
19
+ throw new MutationWorkerError("MUTATION_FAILED", "Missing request.");
20
+ }
21
+ let parsed;
22
+ try {
23
+ parsed = JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
24
+ }
25
+ catch (error) {
26
+ throw new MutationWorkerError("MUTATION_FAILED", `Invalid request: ${String(error)}`);
27
+ }
28
+ if (typeof parsed !== "object" ||
29
+ parsed === null ||
30
+ !("action" in parsed) ||
31
+ !["mkdir", "remove", "rmdir", "write"].includes(String(parsed.action)) ||
32
+ !("targetName" in parsed) ||
33
+ typeof parsed.targetName !== "string" ||
34
+ parsed.targetName.length === 0 ||
35
+ parsed.targetName === "." ||
36
+ parsed.targetName === ".." ||
37
+ parsed.targetName.includes("/") ||
38
+ parsed.targetName.includes("\\") ||
39
+ parsed.targetName.includes("\0") ||
40
+ !("expectedHash" in parsed) ||
41
+ (parsed.expectedHash !== null &&
42
+ (typeof parsed.expectedHash !== "string" ||
43
+ !/^[a-f0-9]{64}$/.test(parsed.expectedHash))) ||
44
+ !("expectedParentPath" in parsed) ||
45
+ typeof parsed.expectedParentPath !== "string" ||
46
+ !("parentDevice" in parsed) ||
47
+ typeof parsed.parentDevice !== "string" ||
48
+ !("parentInode" in parsed) ||
49
+ typeof parsed.parentInode !== "string" ||
50
+ ((parsed.action === "write" || parsed.action === "mkdir") &&
51
+ (!("mode" in parsed) ||
52
+ !Number.isInteger(parsed.mode) ||
53
+ parsed.mode < 0 ||
54
+ parsed.mode > 0o777))) {
55
+ throw new MutationWorkerError("MUTATION_FAILED", "Invalid request.");
56
+ }
57
+ return parsed;
58
+ }
59
+ function normalizedPath(path) {
60
+ return process.platform === "win32" ? path.toLowerCase() : path;
61
+ }
62
+ async function assertAnchoredParent(request) {
63
+ const [canonicalPath, status] = await Promise.all([
64
+ realpath("."),
65
+ stat(".", { bigint: true })
66
+ ]);
67
+ if (!status.isDirectory() ||
68
+ normalizedPath(canonicalPath) !==
69
+ normalizedPath(request.expectedParentPath) ||
70
+ status.dev.toString() !== request.parentDevice ||
71
+ status.ino.toString() !== request.parentInode) {
72
+ throw new MutationWorkerError("PRECONDITION_CHANGED", "The destination directory changed before mutation.");
73
+ }
74
+ }
75
+ async function currentHash(targetName) {
76
+ try {
77
+ const status = await lstat(targetName);
78
+ if (!status.isFile() || status.isSymbolicLink()) {
79
+ throw new MutationWorkerError("PRECONDITION_CHANGED", "The managed target is no longer a regular file.");
80
+ }
81
+ return sha256(await readFile(targetName));
82
+ }
83
+ catch (error) {
84
+ if (isMissing(error)) {
85
+ return null;
86
+ }
87
+ throw error;
88
+ }
89
+ }
90
+ async function assertTarget(request) {
91
+ if ((await currentHash(request.targetName)) !== request.expectedHash) {
92
+ throw new MutationWorkerError("PRECONDITION_CHANGED", "The managed target changed before mutation.");
93
+ }
94
+ }
95
+ async function readStandardInput() {
96
+ const chunks = [];
97
+ for await (const chunk of process.stdin) {
98
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
99
+ }
100
+ return Buffer.concat(chunks);
101
+ }
102
+ async function syncCurrentDirectory() {
103
+ try {
104
+ const directory = await open(".", "r");
105
+ try {
106
+ await directory.sync();
107
+ }
108
+ finally {
109
+ await directory.close();
110
+ }
111
+ }
112
+ catch (error) {
113
+ if (typeof error === "object" &&
114
+ error !== null &&
115
+ "code" in error &&
116
+ ["EACCES", "EISDIR", "EINVAL", "ENOTSUP", "EPERM"].includes(String(error.code))) {
117
+ return;
118
+ }
119
+ throw error;
120
+ }
121
+ }
122
+ async function writeTarget(request) {
123
+ const mode = request.mode;
124
+ if (mode === undefined) {
125
+ throw new MutationWorkerError("MUTATION_FAILED", "Missing write mode.");
126
+ }
127
+ const temporaryName = `.${request.targetName}.agent-ops-tmp-${randomUUID()}`;
128
+ let handle;
129
+ try {
130
+ const content = await readStandardInput();
131
+ handle = await open(temporaryName, "wx", 0o600);
132
+ await handle.writeFile(content);
133
+ await handle.sync();
134
+ await handle.close();
135
+ handle = undefined;
136
+ await chmod(temporaryName, mode);
137
+ await assertAnchoredParent(request);
138
+ await assertTarget(request);
139
+ await rename(temporaryName, request.targetName);
140
+ await syncCurrentDirectory();
141
+ }
142
+ catch (error) {
143
+ await handle?.close().catch(() => undefined);
144
+ await rm(temporaryName, { force: true }).catch(() => undefined);
145
+ throw error;
146
+ }
147
+ }
148
+ async function run() {
149
+ const request = parseRequest(process.argv[2]);
150
+ await assertAnchoredParent(request);
151
+ if (request.action === "write") {
152
+ await assertTarget(request);
153
+ await writeTarget(request);
154
+ }
155
+ else if (request.action === "remove") {
156
+ await assertTarget(request);
157
+ await unlink(request.targetName);
158
+ await syncCurrentDirectory();
159
+ }
160
+ else if (request.action === "mkdir") {
161
+ await assertTarget(request);
162
+ const mode = request.mode;
163
+ if (mode === undefined) {
164
+ throw new MutationWorkerError("MUTATION_FAILED", "Missing directory mode.");
165
+ }
166
+ await mkdir(request.targetName, { mode });
167
+ await chmod(request.targetName, mode);
168
+ await syncCurrentDirectory();
169
+ }
170
+ else {
171
+ const status = await lstat(request.targetName);
172
+ if (!status.isDirectory() || status.isSymbolicLink()) {
173
+ throw new MutationWorkerError("PRECONDITION_CHANGED", "The cleanup target is no longer a directory.");
174
+ }
175
+ await rmdir(request.targetName);
176
+ await syncCurrentDirectory();
177
+ }
178
+ }
179
+ run().catch((error) => {
180
+ const code = error instanceof MutationWorkerError
181
+ ? error.code
182
+ : "MUTATION_FAILED";
183
+ process.stderr.write(`${code}\n`);
184
+ process.exitCode = code === "PRECONDITION_CHANGED" ? 10 : 1;
185
+ });
@@ -0,0 +1,96 @@
1
+ import { lstat, realpath } from "node:fs/promises";
2
+ import { isAbsolute, join, relative, resolve, sep, win32 } from "node:path";
3
+ const WINDOWS_RESERVED_SEGMENT = /^(?:aux|com[1-9]|con|lpt[1-9]|nul|prn)(?:\..*)?$/i;
4
+ export class AgentOpsError extends Error {
5
+ code;
6
+ cause;
7
+ recoveryPaths;
8
+ constructor(code, message, options) {
9
+ super(message);
10
+ this.name = "AgentOpsError";
11
+ this.code = code;
12
+ this.cause = options?.cause;
13
+ this.recoveryPaths = options?.recoveryPaths;
14
+ }
15
+ }
16
+ function isMissing(error) {
17
+ return (typeof error === "object" &&
18
+ error !== null &&
19
+ "code" in error &&
20
+ error.code === "ENOENT");
21
+ }
22
+ function assertPortableRelativePath(path) {
23
+ if (path.length === 0 ||
24
+ path.includes("\0") ||
25
+ path.includes("\\") ||
26
+ isAbsolute(path) ||
27
+ /^[A-Za-z]:/.test(path)) {
28
+ throw new AgentOpsError("PATH_OUTSIDE_ROOT", `Path must be portable and relative: ${path}`);
29
+ }
30
+ const segments = path.split("/");
31
+ if (segments.some((segment) => segment.length === 0 ||
32
+ segment === "." ||
33
+ segment === ".." ||
34
+ segment.endsWith(".") ||
35
+ WINDOWS_RESERVED_SEGMENT.test(segment) ||
36
+ !/^[A-Za-z0-9._-]+$/.test(segment))) {
37
+ throw new AgentOpsError("PATH_OUTSIDE_ROOT", `Path contains an unsafe segment: ${path}`);
38
+ }
39
+ return segments;
40
+ }
41
+ function assertContained(root, candidate) {
42
+ const fromRoot = relative(root, candidate);
43
+ if (fromRoot === "" ||
44
+ fromRoot === ".." ||
45
+ fromRoot.startsWith(`..${sep}`) ||
46
+ isAbsolute(fromRoot)) {
47
+ throw new AgentOpsError("PATH_OUTSIDE_ROOT", `Resolved path escapes the managed root: ${candidate}`);
48
+ }
49
+ }
50
+ export function sameResolvedPath(left, right, platform = process.platform) {
51
+ if (platform !== "win32") {
52
+ return left === right;
53
+ }
54
+ return (win32.normalize(left).toLowerCase() ===
55
+ win32.normalize(right).toLowerCase());
56
+ }
57
+ export async function resolveContainedPath(root, path) {
58
+ const segments = assertPortableRelativePath(path);
59
+ const canonicalRoot = await realpath(resolve(root));
60
+ let current = canonicalRoot;
61
+ for (let index = 0; index < segments.length; index += 1) {
62
+ const candidate = join(current, segments[index]);
63
+ let status;
64
+ try {
65
+ status = await lstat(candidate);
66
+ }
67
+ catch (error) {
68
+ if (!isMissing(error)) {
69
+ throw error;
70
+ }
71
+ const unresolved = join(current, ...segments.slice(index));
72
+ assertContained(canonicalRoot, unresolved);
73
+ return unresolved;
74
+ }
75
+ if (status.isSymbolicLink()) {
76
+ try {
77
+ const linked = await realpath(candidate);
78
+ assertContained(canonicalRoot, linked);
79
+ current = linked;
80
+ }
81
+ catch (error) {
82
+ if (error instanceof AgentOpsError) {
83
+ throw error;
84
+ }
85
+ throw new AgentOpsError("PATH_OUTSIDE_ROOT", `Symlink target cannot be resolved safely: ${path}`, { cause: error });
86
+ }
87
+ }
88
+ else {
89
+ const canonical = await realpath(candidate);
90
+ assertContained(canonicalRoot, canonical);
91
+ current = canonical;
92
+ }
93
+ }
94
+ assertContained(canonicalRoot, current);
95
+ return current;
96
+ }