@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,8 @@
1
+ export const CLAUDE_SUPPORTED_EVENTS = [
2
+ "SessionStart",
3
+ "PreToolUse",
4
+ "Stop"
5
+ ];
6
+ export function claudeNonInteractiveTrust(printMode) {
7
+ return printMode ? "dialog-skipped" : "interactive-dialog";
8
+ }
@@ -0,0 +1,38 @@
1
+ import { normalizeHookEvent } from "../../hooks/normalize.js";
2
+ import { normalizeShellHookEvent } from "../../hooks/shell.js";
3
+ function isRecord(value) {
4
+ return typeof value === "object" && value !== null && !Array.isArray(value);
5
+ }
6
+ export function claudeStopRecursionMarker(input) {
7
+ return (isRecord(input) &&
8
+ input.hook_event_name === "Stop" &&
9
+ input.stop_hook_active === true);
10
+ }
11
+ export function normalizeClaudeHookInput(input) {
12
+ if (!isRecord(input)) {
13
+ return normalizeHookEvent(input);
14
+ }
15
+ const projectRoot = input.cwd;
16
+ if (input.hook_event_name === "SessionStart") {
17
+ return normalizeHookEvent({
18
+ event: "session-start",
19
+ projectRoot
20
+ });
21
+ }
22
+ if (input.hook_event_name === "Stop") {
23
+ return normalizeHookEvent({
24
+ event: "stop",
25
+ projectRoot
26
+ });
27
+ }
28
+ if (input.hook_event_name === "PreToolUse" &&
29
+ input.tool_name === "Bash" &&
30
+ isRecord(input.tool_input) &&
31
+ typeof input.tool_input.command === "string") {
32
+ return normalizeShellHookEvent(input.tool_input.command, projectRoot);
33
+ }
34
+ return normalizeHookEvent({
35
+ event: "unsupported",
36
+ projectRoot
37
+ });
38
+ }
@@ -0,0 +1,30 @@
1
+ function json(value) {
2
+ return {
3
+ exitCode: 0,
4
+ stdout: JSON.stringify(value),
5
+ stderr: ""
6
+ };
7
+ }
8
+ export function claudeHookOutput(event, result) {
9
+ if (result.action === "continue" && result.status === "PASS") {
10
+ return { exitCode: 0, stdout: "", stderr: "" };
11
+ }
12
+ if (event === "PreToolUse" && result.action === "block") {
13
+ return json({
14
+ hookSpecificOutput: {
15
+ hookEventName: "PreToolUse",
16
+ permissionDecision: "deny",
17
+ permissionDecisionReason: result.code
18
+ }
19
+ });
20
+ }
21
+ if (event === "Stop" && result.action === "block") {
22
+ return json({
23
+ decision: "block",
24
+ reason: result.code
25
+ });
26
+ }
27
+ return json({
28
+ systemMessage: `agent-ops: ${result.code}`
29
+ });
30
+ }
@@ -0,0 +1,93 @@
1
+ import { AgentOpsError } from "../../fs/paths.js";
2
+ const COMMAND_PREFIX = "agent-ops hook codex ";
3
+ function isRecord(value) {
4
+ return typeof value === "object" && value !== null && !Array.isArray(value);
5
+ }
6
+ function commandHook(event) {
7
+ const command = `${COMMAND_PREFIX}${event}`;
8
+ return {
9
+ type: "command",
10
+ command,
11
+ commandWindows: command,
12
+ timeout: 30,
13
+ statusMessage: `Running agent-ops ${event}`
14
+ };
15
+ }
16
+ function matcherGroup(event) {
17
+ return {
18
+ ...(event === "PreToolUse" ? { matcher: "^Bash$" } : {}),
19
+ hooks: [commandHook(event)]
20
+ };
21
+ }
22
+ export function codexHookTarget(scope) {
23
+ return {
24
+ path: ".codex/hooks.json",
25
+ representation: "json",
26
+ requiresProjectTrust: scope === "project"
27
+ };
28
+ }
29
+ export function buildCodexHookConfig(capabilities) {
30
+ const hooks = {};
31
+ if (capabilities.includes("lifecycle-summary")) {
32
+ hooks.SessionStart = [matcherGroup("SessionStart")];
33
+ }
34
+ if (capabilities.includes("command-policy")) {
35
+ hooks.PreToolUse = [matcherGroup("PreToolUse")];
36
+ }
37
+ if (capabilities.includes("optional-stop-verify")) {
38
+ hooks.Stop = [matcherGroup("Stop")];
39
+ }
40
+ return {
41
+ description: "Portable agent-ops lifecycle hooks.",
42
+ hooks
43
+ };
44
+ }
45
+ function isOwnedHandler(hook) {
46
+ return (isRecord(hook) &&
47
+ typeof hook.command === "string" &&
48
+ hook.command.startsWith(COMMAND_PREFIX));
49
+ }
50
+ function withoutOwnedHandlers(value) {
51
+ if (!isRecord(value) || !Array.isArray(value.hooks)) {
52
+ return value;
53
+ }
54
+ const hooks = value.hooks.filter((hook) => !isOwnedHandler(hook));
55
+ return hooks.length === 0 ? null : { ...value, hooks };
56
+ }
57
+ function hookRecord(value) {
58
+ if (!isRecord(value)) {
59
+ throw new AgentOpsError("CODEX_HOOK_CONFIG_INVALID", "Codex hook configuration must be a JSON object.");
60
+ }
61
+ if (value.hooks === undefined) {
62
+ return {};
63
+ }
64
+ if (!isRecord(value.hooks) ||
65
+ Object.values(value.hooks).some((groups) => !Array.isArray(groups))) {
66
+ throw new AgentOpsError("CODEX_HOOK_CONFIG_INVALID", "Codex hook groups must be arrays.");
67
+ }
68
+ return value.hooks;
69
+ }
70
+ export function mergeCodexHookConfig(existing, managed) {
71
+ if (!isRecord(existing)) {
72
+ throw new AgentOpsError("CODEX_HOOK_CONFIG_INVALID", "Codex hook configuration must be a JSON object.");
73
+ }
74
+ const existingHooks = hookRecord(existing);
75
+ const hooks = {};
76
+ const eventNames = new Set([
77
+ ...Object.keys(existingHooks),
78
+ ...Object.keys(managed.hooks)
79
+ ]);
80
+ for (const eventName of eventNames) {
81
+ const preserved = (existingHooks[eventName] ?? [])
82
+ .map(withoutOwnedHandlers)
83
+ .filter((group) => group !== null);
84
+ const additions = managed.hooks[eventName] ?? [];
85
+ if (preserved.length > 0 || additions.length > 0) {
86
+ hooks[eventName] = [...preserved, ...additions];
87
+ }
88
+ }
89
+ return {
90
+ ...existing,
91
+ hooks
92
+ };
93
+ }
@@ -0,0 +1,8 @@
1
+ export const CODEX_SUPPORTED_EVENTS = [
2
+ "SessionStart",
3
+ "PreToolUse",
4
+ "Stop"
5
+ ];
6
+ export function codexMatcherSupport(event) {
7
+ return event === "PreToolUse" ? "tool-name" : "unsupported";
8
+ }
@@ -0,0 +1,41 @@
1
+ import { normalizeHookEvent } from "../../hooks/normalize.js";
2
+ import { normalizeShellHookEvent } from "../../hooks/shell.js";
3
+ function isRecord(value) {
4
+ return typeof value === "object" && value !== null && !Array.isArray(value);
5
+ }
6
+ function shellCommand(input) {
7
+ if (!isRecord(input.tool_input)) {
8
+ return null;
9
+ }
10
+ const candidate = input.tool_input.command ?? input.tool_input.cmd;
11
+ return typeof candidate === "string" ? candidate : null;
12
+ }
13
+ export function normalizeCodexHookInput(input) {
14
+ if (!isRecord(input)) {
15
+ return normalizeHookEvent(input);
16
+ }
17
+ const projectRoot = input.cwd;
18
+ if (input.hook_event_name === "SessionStart") {
19
+ return normalizeHookEvent({
20
+ event: "session-start",
21
+ projectRoot
22
+ });
23
+ }
24
+ if (input.hook_event_name === "Stop") {
25
+ return normalizeHookEvent({
26
+ event: "stop",
27
+ projectRoot
28
+ });
29
+ }
30
+ if (input.hook_event_name === "PreToolUse" &&
31
+ input.tool_name === "Bash") {
32
+ const rawCommand = shellCommand(input);
33
+ if (rawCommand !== null) {
34
+ return normalizeShellHookEvent(rawCommand, projectRoot);
35
+ }
36
+ }
37
+ return normalizeHookEvent({
38
+ event: "unsupported",
39
+ projectRoot
40
+ });
41
+ }
@@ -0,0 +1,24 @@
1
+ export const CODEX_NON_ZERO_EXIT_BEHAVIOR = "UNKNOWN";
2
+ export const CODEX_PRE_TOOL_BLOCKING = "UNKNOWN";
3
+ export function codexHookOutput(event, result) {
4
+ if (result.action === "continue" && result.status === "PASS") {
5
+ return { exitCode: 0, stdout: "" };
6
+ }
7
+ if (event === "PreToolUse") {
8
+ return {
9
+ exitCode: 0,
10
+ stdout: JSON.stringify({
11
+ systemMessage: `agent-ops: ${result.code}`
12
+ })
13
+ };
14
+ }
15
+ return {
16
+ exitCode: 0,
17
+ stdout: JSON.stringify({
18
+ continue: result.action !== "block",
19
+ ...(result.action === "block"
20
+ ? { stopReason: result.code }
21
+ : { systemMessage: `agent-ops: ${result.code}` })
22
+ })
23
+ };
24
+ }
@@ -0,0 +1,34 @@
1
+ export function explainConfig(merged) {
2
+ return {
3
+ schemaVersion: merged.config.schemaVersion,
4
+ profiles: merged.provenance.profiles.map((entry) => ({
5
+ id: entry.value,
6
+ source: entry.source,
7
+ sourcePath: entry.sourcePath
8
+ })),
9
+ verificationCommands: merged.provenance.verificationCommands.map((entry) => ({
10
+ id: entry.value.id,
11
+ required: entry.value.required,
12
+ shell: entry.value.shell === true,
13
+ evidence: {
14
+ kind: entry.value.evidence.kind,
15
+ minimum: entry.value.evidence.minimum ?? null
16
+ },
17
+ source: entry.source,
18
+ sourcePath: entry.sourcePath
19
+ })),
20
+ pathMappings: merged.provenance.pathMappings.map((entry) => ({
21
+ path: entry.value.path,
22
+ verifierIds: [...entry.value.verifierIds],
23
+ source: entry.source,
24
+ sourcePath: entry.sourcePath
25
+ })),
26
+ securityExceptions: merged.provenance.securityExceptions.map((entry) => ({
27
+ ruleId: entry.value.ruleId,
28
+ scope: entry.value.scope,
29
+ expiresAt: entry.value.expiresAt,
30
+ source: entry.source,
31
+ sourcePath: entry.sourcePath
32
+ }))
33
+ };
34
+ }
@@ -0,0 +1,25 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { AgentOpsError } from "../fs/paths.js";
3
+ import { previewConfigMigration } from "./migrate.js";
4
+ export async function loadConfigFile(path) {
5
+ let source;
6
+ try {
7
+ source = await readFile(path, "utf8");
8
+ }
9
+ catch (error) {
10
+ throw new AgentOpsError("CONFIG_READ_FAILED", `Unable to read config: ${path}`, { cause: error });
11
+ }
12
+ let parsed;
13
+ try {
14
+ parsed = JSON.parse(source);
15
+ }
16
+ catch (error) {
17
+ throw new AgentOpsError("CONFIG_PARSE_FAILED", `Unable to parse config JSON: ${path}`, { cause: error });
18
+ }
19
+ const preview = previewConfigMigration(parsed);
20
+ return {
21
+ config: preview.migrated,
22
+ sourcePath: path,
23
+ migration: { steps: preview.steps }
24
+ };
25
+ }
@@ -0,0 +1,170 @@
1
+ import { AgentOpsError } from "../fs/paths.js";
2
+ import { validateConfig } from "../schema/validate.js";
3
+ const SOURCE_RANK = {
4
+ default: 0,
5
+ user: 1,
6
+ project: 2
7
+ };
8
+ function clone(value) {
9
+ return structuredClone(value);
10
+ }
11
+ function mappingKey(mapping) {
12
+ return mapping.path.toLowerCase();
13
+ }
14
+ function exceptionKey(exception) {
15
+ return `${exception.ruleId}\0${exception.scope.toLowerCase()}`;
16
+ }
17
+ function sameException(left, right) {
18
+ return (left.ruleId === right.ruleId &&
19
+ left.scope === right.scope &&
20
+ left.expiresAt === right.expiresAt &&
21
+ left.reason === right.reason);
22
+ }
23
+ function assertUniqueKeys(values, keyOf, layer, collection) {
24
+ const keys = new Set();
25
+ for (const value of values) {
26
+ const key = keyOf(value);
27
+ if (keys.has(key)) {
28
+ throw new AgentOpsError("CONFIG_DUPLICATE_ID", `Duplicate ${collection} stable ID in ${layer.source} config.`);
29
+ }
30
+ keys.add(key);
31
+ }
32
+ }
33
+ function assertLayer(layer) {
34
+ const validation = validateConfig(layer.config);
35
+ if (!validation.ok) {
36
+ const first = validation.errors[0];
37
+ throw new AgentOpsError(first?.code === "DUPLICATE_ID"
38
+ ? "CONFIG_DUPLICATE_ID"
39
+ : "CONFIG_INVALID", `Invalid ${layer.source} config${first === undefined ? "." : `: ${first.code} at ${first.path}.`}`);
40
+ }
41
+ assertUniqueKeys(layer.config.verification.commands, (command) => command.id, layer, "verification command");
42
+ assertUniqueKeys(layer.config.pathMappings, mappingKey, layer, "path mapping");
43
+ assertUniqueKeys(layer.config.securityExceptions, exceptionKey, layer, "security exception");
44
+ }
45
+ function assertProjectCommandIsMonotonic(existing, candidate) {
46
+ if (existing === undefined ||
47
+ existing.source === "project") {
48
+ return;
49
+ }
50
+ const previous = existing.value;
51
+ const previousMinimum = previous.evidence.minimum ?? 0;
52
+ const candidateMinimum = candidate.evidence.minimum ?? 0;
53
+ const weakensRequired = previous.required && !candidate.required;
54
+ const enablesShell = previous.shell !== true && candidate.shell === true;
55
+ const changesProtectedExecution = previous.required &&
56
+ (previous.command !== candidate.command ||
57
+ previous.cwd !== candidate.cwd ||
58
+ (previous.shell === true) !== (candidate.shell === true) ||
59
+ previous.args.length !== candidate.args.length ||
60
+ previous.args.some((argument, index) => argument !== candidate.args[index]));
61
+ const lowersEvidence = previous.required &&
62
+ (previous.evidence.kind !== candidate.evidence.kind ||
63
+ candidateMinimum < previousMinimum);
64
+ const weakensTimeout = previous.required &&
65
+ previous.timeoutMs !== undefined &&
66
+ (candidate.timeoutMs === undefined ||
67
+ candidate.timeoutMs > previous.timeoutMs);
68
+ if (weakensRequired ||
69
+ enablesShell ||
70
+ changesProtectedExecution ||
71
+ lowersEvidence ||
72
+ weakensTimeout) {
73
+ throw new AgentOpsError("PROJECT_GUARDRAIL_WEAKENING", `Project command cannot weaken protected verifier ID: ${candidate.id}`);
74
+ }
75
+ }
76
+ function assertProjectMappingIsMonotonic(existing, candidate) {
77
+ if (existing === undefined || existing.source === "project") {
78
+ return;
79
+ }
80
+ const candidateIds = new Set(candidate.verifierIds);
81
+ if (existing.value.path !== candidate.path ||
82
+ existing.value.verifierIds.some((verifierId) => !candidateIds.has(verifierId))) {
83
+ throw new AgentOpsError("PROJECT_GUARDRAIL_WEAKENING", `Project mapping cannot drop verifier coverage for: ${candidate.path}`);
84
+ }
85
+ }
86
+ function effective(value, layer) {
87
+ return {
88
+ value: clone(value),
89
+ source: layer.source,
90
+ sourcePath: layer.sourcePath
91
+ };
92
+ }
93
+ export function mergeConfigLayers(inputLayers) {
94
+ if (inputLayers.length === 0) {
95
+ throw new AgentOpsError("CONFIG_LAYER_REQUIRED", "At least one configuration layer is required.");
96
+ }
97
+ const sources = new Set();
98
+ for (const layer of inputLayers) {
99
+ if (sources.has(layer.source)) {
100
+ throw new AgentOpsError("CONFIG_LAYER_DUPLICATE", `Configuration source may appear only once: ${layer.source}`);
101
+ }
102
+ sources.add(layer.source);
103
+ assertLayer(layer);
104
+ }
105
+ const layers = [...inputLayers].sort((left, right) => SOURCE_RANK[left.source] - SOURCE_RANK[right.source]);
106
+ const profiles = new Map();
107
+ const commands = new Map();
108
+ const mappings = new Map();
109
+ const exceptions = new Map();
110
+ let schemaVersion;
111
+ for (const layer of layers) {
112
+ schemaVersion = effective(layer.config.schemaVersion, layer);
113
+ for (const profile of layer.config.profiles) {
114
+ profiles.set(profile, effective(profile, layer));
115
+ }
116
+ for (const command of layer.config.verification.commands) {
117
+ if (layer.source === "project") {
118
+ assertProjectCommandIsMonotonic(commands.get(command.id), command);
119
+ }
120
+ commands.set(command.id, effective(command, layer));
121
+ }
122
+ for (const mapping of layer.config.pathMappings) {
123
+ const key = mappingKey(mapping);
124
+ if (layer.source === "project") {
125
+ assertProjectMappingIsMonotonic(mappings.get(key), mapping);
126
+ }
127
+ mappings.set(key, effective(mapping, layer));
128
+ }
129
+ for (const securityException of layer.config.securityExceptions) {
130
+ const key = exceptionKey(securityException);
131
+ const existing = exceptions.get(key);
132
+ if (layer.source === "project") {
133
+ if (existing === undefined ||
134
+ !sameException(existing.value, securityException)) {
135
+ throw new AgentOpsError("PROJECT_SECURITY_WEAKENING", `Project config cannot authorize security exception: ${securityException.ruleId}`);
136
+ }
137
+ continue;
138
+ }
139
+ exceptions.set(key, effective(securityException, layer));
140
+ }
141
+ }
142
+ if (schemaVersion === undefined) {
143
+ throw new AgentOpsError("CONFIG_LAYER_REQUIRED", "At least one configuration layer is required.");
144
+ }
145
+ const provenance = {
146
+ schemaVersion,
147
+ profiles: [...profiles.values()],
148
+ verificationCommands: [...commands.values()],
149
+ pathMappings: [...mappings.values()],
150
+ securityExceptions: [...exceptions.values()]
151
+ };
152
+ const config = {
153
+ schemaVersion: 1,
154
+ profiles: provenance.profiles.map(({ value }) => value),
155
+ verification: {
156
+ commands: provenance.verificationCommands.map(({ value }) => value)
157
+ },
158
+ pathMappings: provenance.pathMappings.map(({ value }) => value),
159
+ securityExceptions: provenance.securityExceptions.map(({ value }) => value)
160
+ };
161
+ const validation = validateConfig(config);
162
+ if (!validation.ok) {
163
+ const first = validation.errors[0];
164
+ throw new AgentOpsError("CONFIG_MERGE_INVALID", `Merged config is invalid${first === undefined ? "." : `: ${first.code} at ${first.path}.`}`);
165
+ }
166
+ return {
167
+ config: clone(validation.value),
168
+ provenance: clone(provenance)
169
+ };
170
+ }
@@ -0,0 +1,61 @@
1
+ import { SCHEMA_VERSION } from "../contracts.js";
2
+ import { AgentOpsError } from "../fs/paths.js";
3
+ import { validateConfig } from "../schema/validate.js";
4
+ function isRecord(value) {
5
+ return typeof value === "object" && value !== null && !Array.isArray(value);
6
+ }
7
+ function clone(value) {
8
+ return structuredClone(value);
9
+ }
10
+ const MIGRATIONS = new Map([
11
+ [
12
+ 0,
13
+ (input) => {
14
+ const { commands, schemaVersion: _schemaVersion, ...rest } = input;
15
+ return {
16
+ ...rest,
17
+ schemaVersion: 1,
18
+ verification: { commands: clone(commands) }
19
+ };
20
+ }
21
+ ]
22
+ ]);
23
+ function schemaVersionOf(value) {
24
+ if (!isRecord(value) ||
25
+ !Number.isSafeInteger(value.schemaVersion) ||
26
+ value.schemaVersion < 0) {
27
+ throw new AgentOpsError("CONFIG_SCHEMA_INVALID", "Config schemaVersion must be a non-negative integer.");
28
+ }
29
+ return value.schemaVersion;
30
+ }
31
+ export function previewConfigMigration(input) {
32
+ const sourceVersion = schemaVersionOf(input);
33
+ if (sourceVersion > SCHEMA_VERSION) {
34
+ throw new AgentOpsError("CONFIG_SCHEMA_FUTURE", `Config schemaVersion ${sourceVersion} is newer than supported version ${SCHEMA_VERSION}.`);
35
+ }
36
+ let migrated = clone(input);
37
+ let version = sourceVersion;
38
+ const steps = [];
39
+ while (version < SCHEMA_VERSION) {
40
+ const migration = MIGRATIONS.get(version);
41
+ if (migration === undefined || !isRecord(migrated)) {
42
+ throw new AgentOpsError("CONFIG_MIGRATION_MISSING", `No migration is registered from config schemaVersion ${version}.`);
43
+ }
44
+ const nextVersion = version + 1;
45
+ migrated = migration(clone(migrated));
46
+ if (schemaVersionOf(migrated) !== nextVersion) {
47
+ throw new AgentOpsError("CONFIG_MIGRATION_INVALID", `Migration ${version} must produce schemaVersion ${nextVersion}.`);
48
+ }
49
+ steps.push({ fromVersion: version, toVersion: nextVersion });
50
+ version = nextVersion;
51
+ }
52
+ const validation = validateConfig(migrated);
53
+ if (!validation.ok) {
54
+ const first = validation.errors[0];
55
+ throw new AgentOpsError("CONFIG_INVALID", `Migrated config is invalid${first === undefined ? "." : `: ${first.code} at ${first.path}.`}`);
56
+ }
57
+ return {
58
+ migrated: clone(validation.value),
59
+ steps
60
+ };
61
+ }
@@ -0,0 +1 @@
1
+ export const SCHEMA_VERSION = 1;
@@ -0,0 +1,145 @@
1
+ import { constants } from "node:fs";
2
+ import { lstat, open } from "node:fs/promises";
3
+ import path from "node:path";
4
+ const MAX_GO_MOD_BYTES = 1024 * 1024;
5
+ function hasErrorCode(error, code) {
6
+ return (error instanceof Error &&
7
+ "code" in error &&
8
+ error.code === code);
9
+ }
10
+ async function readGoMod(filePath) {
11
+ let status;
12
+ try {
13
+ status = await lstat(filePath);
14
+ }
15
+ catch (error) {
16
+ if (hasErrorCode(error, "ENOENT")) {
17
+ return { kind: "missing" };
18
+ }
19
+ return {
20
+ kind: "invalid",
21
+ message: "go.mod could not be inspected safely."
22
+ };
23
+ }
24
+ if (status.isSymbolicLink() || !status.isFile()) {
25
+ return {
26
+ kind: "invalid",
27
+ message: "go.mod must be a regular file."
28
+ };
29
+ }
30
+ if (status.size > MAX_GO_MOD_BYTES) {
31
+ return {
32
+ kind: "invalid",
33
+ message: "go.mod exceeds the discovery size limit."
34
+ };
35
+ }
36
+ let handle;
37
+ try {
38
+ handle = await open(filePath, constants.O_RDONLY |
39
+ constants.O_NOFOLLOW |
40
+ constants.O_NONBLOCK);
41
+ const openedStatus = await handle.stat();
42
+ if (!openedStatus.isFile()) {
43
+ return {
44
+ kind: "invalid",
45
+ message: "go.mod must remain a regular file."
46
+ };
47
+ }
48
+ const chunks = [];
49
+ let totalBytes = 0;
50
+ while (totalBytes <= MAX_GO_MOD_BYTES) {
51
+ const remaining = MAX_GO_MOD_BYTES + 1 - totalBytes;
52
+ const buffer = Buffer.alloc(Math.min(64 * 1024, remaining));
53
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, null);
54
+ if (bytesRead === 0) {
55
+ return {
56
+ kind: "source",
57
+ source: Buffer.concat(chunks, totalBytes).toString("utf8")
58
+ };
59
+ }
60
+ chunks.push(buffer.subarray(0, bytesRead));
61
+ totalBytes += bytesRead;
62
+ }
63
+ return {
64
+ kind: "invalid",
65
+ message: "go.mod exceeds the discovery size limit."
66
+ };
67
+ }
68
+ catch {
69
+ return {
70
+ kind: "invalid",
71
+ message: "go.mod could not be read as a regular file."
72
+ };
73
+ }
74
+ finally {
75
+ await handle?.close().catch(() => undefined);
76
+ }
77
+ }
78
+ function hasOneModuleDirective(source) {
79
+ if (source.includes("\0") || source.includes("\uFFFD")) {
80
+ return false;
81
+ }
82
+ const moduleLines = source.split(/\r?\n/u).filter((line) => (/^\s*module(?:\s|$)/u.test(line)));
83
+ return (moduleLines.length === 1 &&
84
+ /^\s*module\s+\S+\s*(?:(?:\/\/).*)?$/u.test(moduleLines[0] ?? ""));
85
+ }
86
+ function fileEvidence(detail) {
87
+ return { kind: "file", path: "go.mod", detail };
88
+ }
89
+ function userDecision(reason, message, evidence) {
90
+ return {
91
+ kind: "user-decision",
92
+ adapter: "go",
93
+ reason,
94
+ message,
95
+ evidence,
96
+ manualConfigAllowed: true
97
+ };
98
+ }
99
+ function goTestProposal(evidence) {
100
+ return {
101
+ id: "go:test",
102
+ command: "go",
103
+ args: ["test", "./..."],
104
+ cwd: ".",
105
+ required: true,
106
+ evidence: { kind: "test-count", minimum: 1 },
107
+ sourceEvidence: evidence,
108
+ confidence: "high",
109
+ confirmed: false
110
+ };
111
+ }
112
+ export async function discoverGoProject(root) {
113
+ const metadata = await readGoMod(path.join(root, "go.mod"));
114
+ if (metadata.kind === "missing") {
115
+ return {
116
+ kind: "no-match",
117
+ adapter: "go",
118
+ reason: "not-go-project",
119
+ message: "No go.mod was found.",
120
+ evidence: [],
121
+ manualConfigAllowed: true
122
+ };
123
+ }
124
+ const evidence = [fileEvidence("Found bounded Go module metadata.")];
125
+ if (metadata.kind === "invalid") {
126
+ return userDecision("invalid-manifest", metadata.message, evidence);
127
+ }
128
+ if (!hasOneModuleDirective(metadata.source)) {
129
+ return userDecision("invalid-manifest", "go.mod does not contain one valid module directive.", evidence);
130
+ }
131
+ const proposalEvidence = [
132
+ fileEvidence("Found one explicit Go module directive.")
133
+ ];
134
+ return {
135
+ kind: "proposals",
136
+ adapter: "go",
137
+ proposals: [goTestProposal(proposalEvidence)],
138
+ evidence: proposalEvidence,
139
+ manualConfigAllowed: true
140
+ };
141
+ }
142
+ export const goDiscoveryAdapter = {
143
+ id: "go",
144
+ discover: discoverGoProject
145
+ };