@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.
- package/LICENSE +21 -0
- package/README.md +83 -0
- package/SECURITY.md +22 -0
- package/dist/packages/cli/src/args.js +322 -0
- package/dist/packages/cli/src/bin.js +290 -0
- package/dist/packages/cli/src/cli.js +80 -0
- package/dist/packages/cli/src/commands/config.js +8 -0
- package/dist/packages/cli/src/commands/doctor.js +32 -0
- package/dist/packages/cli/src/commands/index.js +16 -0
- package/dist/packages/cli/src/commands/init.js +65 -0
- package/dist/packages/cli/src/commands/review.js +71 -0
- package/dist/packages/cli/src/commands/task.js +141 -0
- package/dist/packages/cli/src/commands/trust.js +48 -0
- package/dist/packages/cli/src/commands/uninstall.js +58 -0
- package/dist/packages/cli/src/commands/update.js +58 -0
- package/dist/packages/cli/src/commands/verify.js +108 -0
- package/dist/packages/cli/src/output.js +53 -0
- package/dist/packages/cli/src/plan-output.js +28 -0
- package/dist/packages/cli/src/wizard.js +75 -0
- package/dist/runtime/src/adapters/claude/config.js +109 -0
- package/dist/runtime/src/adapters/claude/events.js +8 -0
- package/dist/runtime/src/adapters/claude/input.js +38 -0
- package/dist/runtime/src/adapters/claude/output.js +30 -0
- package/dist/runtime/src/adapters/codex/config.js +93 -0
- package/dist/runtime/src/adapters/codex/events.js +8 -0
- package/dist/runtime/src/adapters/codex/input.js +41 -0
- package/dist/runtime/src/adapters/codex/output.js +24 -0
- package/dist/runtime/src/config/explain.js +34 -0
- package/dist/runtime/src/config/load.js +25 -0
- package/dist/runtime/src/config/merge.js +170 -0
- package/dist/runtime/src/config/migrate.js +61 -0
- package/dist/runtime/src/contracts.js +1 -0
- package/dist/runtime/src/discovery/go.js +145 -0
- package/dist/runtime/src/discovery/index.js +40 -0
- package/dist/runtime/src/discovery/make.js +162 -0
- package/dist/runtime/src/discovery/node.js +175 -0
- package/dist/runtime/src/discovery/python.js +163 -0
- package/dist/runtime/src/discovery/rust.js +159 -0
- package/dist/runtime/src/discovery/types.js +1 -0
- package/dist/runtime/src/fs/hash.js +19 -0
- package/dist/runtime/src/fs/managed-block.js +90 -0
- package/dist/runtime/src/fs/manifest.js +24 -0
- package/dist/runtime/src/fs/mutation-worker.js +185 -0
- package/dist/runtime/src/fs/paths.js +96 -0
- package/dist/runtime/src/fs/transaction.js +498 -0
- package/dist/runtime/src/guardrails/destructive.js +207 -0
- package/dist/runtime/src/guardrails/evaluate.js +9 -0
- package/dist/runtime/src/guardrails/exceptions.js +49 -0
- package/dist/runtime/src/guardrails/secrets.js +97 -0
- package/dist/runtime/src/guardrails/types.js +9 -0
- package/dist/runtime/src/hooks/dispatch.js +78 -0
- package/dist/runtime/src/hooks/events.js +1 -0
- package/dist/runtime/src/hooks/hook-entry.js +19 -0
- package/dist/runtime/src/hooks/normalize.js +59 -0
- package/dist/runtime/src/hooks/output.js +12 -0
- package/dist/runtime/src/hooks/shell.js +138 -0
- package/dist/runtime/src/hooks/stop-verify.js +70 -0
- package/dist/runtime/src/install/apply.js +70 -0
- package/dist/runtime/src/install/doctor.js +196 -0
- package/dist/runtime/src/install/harness.js +87 -0
- package/dist/runtime/src/install/ownership.js +84 -0
- package/dist/runtime/src/install/plan.js +257 -0
- package/dist/runtime/src/install/profiles.js +28 -0
- package/dist/runtime/src/install/types.js +1 -0
- package/dist/runtime/src/install/uninstall.js +206 -0
- package/dist/runtime/src/install/update.js +123 -0
- package/dist/runtime/src/logging/local-log.js +158 -0
- package/dist/runtime/src/registry/npm.js +141 -0
- package/dist/runtime/src/review/claude-runner.js +4 -0
- package/dist/runtime/src/review/codex-runner.js +4 -0
- package/dist/runtime/src/review/packet.js +10 -0
- package/dist/runtime/src/review/result.js +24 -0
- package/dist/runtime/src/review/roles.js +3 -0
- package/dist/runtime/src/review/runner.js +45 -0
- package/dist/runtime/src/schema/validate.js +584 -0
- package/dist/runtime/src/security/permissions.js +654 -0
- package/dist/runtime/src/security/redact.js +41 -0
- package/dist/runtime/src/security/trust.js +209 -0
- package/dist/runtime/src/task/render.js +43 -0
- package/dist/runtime/src/task/service.js +235 -0
- package/dist/runtime/src/task/store.js +265 -0
- package/dist/runtime/src/verify/change-surface.js +86 -0
- package/dist/runtime/src/verify/evidence.js +89 -0
- package/dist/runtime/src/verify/fingerprint.js +67 -0
- package/dist/runtime/src/verify/scope.js +69 -0
- package/dist/runtime/src/verify/service.js +217 -0
- package/dist/runtime/src/verify/spawn.js +326 -0
- package/dist/runtime/src/verify/test-count.js +148 -0
- package/docs/en/spec/README.md +13 -0
- package/docs/en/spec/acceptance-and-evidence.md +21 -0
- package/docs/en/spec/delegation.md +21 -0
- package/docs/en/spec/guardrails.md +21 -0
- package/docs/en/spec/harness-adapters.md +21 -0
- package/docs/en/spec/judgment.md +21 -0
- package/docs/en/spec/loop-engineering.md +23 -0
- package/docs/en/spec/maintenance.md +21 -0
- package/docs/en/spec/review.md +21 -0
- package/docs/en/spec/troubleshooting.md +21 -0
- package/docs/zh-TW/spec/README.md +13 -0
- package/docs/zh-TW/spec/acceptance-and-evidence.md +23 -0
- package/docs/zh-TW/spec/delegation.md +23 -0
- package/docs/zh-TW/spec/guardrails.md +23 -0
- package/docs/zh-TW/spec/harness-adapters.md +23 -0
- package/docs/zh-TW/spec/judgment.md +23 -0
- package/docs/zh-TW/spec/loop-engineering.md +23 -0
- package/docs/zh-TW/spec/maintenance.md +23 -0
- package/docs/zh-TW/spec/review.md +23 -0
- package/docs/zh-TW/spec/troubleshooting.md +23 -0
- package/package.json +41 -0
- package/schemas/config.schema.json +231 -0
- package/schemas/evidence.schema.json +115 -0
- package/schemas/manifest.schema.json +116 -0
- package/schemas/task.schema.json +59 -0
- package/templates/common/AGENTS.block.md +3 -0
- package/templates/common/CLAUDE.block.md +3 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { goDiscoveryAdapter } from "./go.js";
|
|
2
|
+
import { makeDiscoveryAdapter } from "./make.js";
|
|
3
|
+
import { nodeDiscoveryAdapter } from "./node.js";
|
|
4
|
+
import { pythonDiscoveryAdapter } from "./python.js";
|
|
5
|
+
import { rustDiscoveryAdapter } from "./rust.js";
|
|
6
|
+
const DEFAULT_ADAPTERS = [
|
|
7
|
+
nodeDiscoveryAdapter,
|
|
8
|
+
pythonDiscoveryAdapter,
|
|
9
|
+
goDiscoveryAdapter,
|
|
10
|
+
rustDiscoveryAdapter,
|
|
11
|
+
makeDiscoveryAdapter
|
|
12
|
+
];
|
|
13
|
+
export async function discoverProject(root, adapters = DEFAULT_ADAPTERS) {
|
|
14
|
+
const results = await Promise.all(adapters.map(async (adapter) => await adapter.discover(root)));
|
|
15
|
+
const matched = results.filter((result) => result.kind !== "no-match");
|
|
16
|
+
const evidence = results.flatMap((result) => result.evidence);
|
|
17
|
+
if (matched.length > 0) {
|
|
18
|
+
return {
|
|
19
|
+
kind: "project",
|
|
20
|
+
adapters: matched.map((result) => result.adapter),
|
|
21
|
+
proposals: matched.flatMap((result) => result.kind === "proposals" ? result.proposals : []),
|
|
22
|
+
decisions: matched.filter((result) => result.kind === "user-decision"),
|
|
23
|
+
evidence,
|
|
24
|
+
manualConfigAllowed: true
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
kind: "no-match",
|
|
29
|
+
adapter: "orchestrator",
|
|
30
|
+
reason: "unsupported-stack",
|
|
31
|
+
message: "No supported technology stack was detected.",
|
|
32
|
+
evidence,
|
|
33
|
+
manualConfigAllowed: true
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export { discoverGoProject, goDiscoveryAdapter } from "./go.js";
|
|
37
|
+
export { discoverMakeProject, makeDiscoveryAdapter } from "./make.js";
|
|
38
|
+
export { discoverNodeProject, nodeDiscoveryAdapter } from "./node.js";
|
|
39
|
+
export { discoverPythonProject, pythonDiscoveryAdapter } from "./python.js";
|
|
40
|
+
export { discoverRustProject, rustDiscoveryAdapter } from "./rust.js";
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import { lstat, open } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
const MAX_MAKEFILE_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 readMakefile(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: "Makefile could not be inspected safely."
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
if (status.isSymbolicLink() || !status.isFile()) {
|
|
25
|
+
return {
|
|
26
|
+
kind: "invalid",
|
|
27
|
+
message: "Makefile must be a regular file."
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
if (status.size > MAX_MAKEFILE_BYTES) {
|
|
31
|
+
return {
|
|
32
|
+
kind: "invalid",
|
|
33
|
+
message: "Makefile exceeds the discovery size limit."
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
let handle;
|
|
37
|
+
try {
|
|
38
|
+
handle = await open(filePath, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
39
|
+
const openedStatus = await handle.stat();
|
|
40
|
+
if (!openedStatus.isFile()) {
|
|
41
|
+
return {
|
|
42
|
+
kind: "invalid",
|
|
43
|
+
message: "Makefile must remain a regular file."
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const chunks = [];
|
|
47
|
+
let totalBytes = 0;
|
|
48
|
+
while (totalBytes <= MAX_MAKEFILE_BYTES) {
|
|
49
|
+
const remaining = MAX_MAKEFILE_BYTES + 1 - totalBytes;
|
|
50
|
+
const buffer = Buffer.alloc(Math.min(64 * 1024, remaining));
|
|
51
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, null);
|
|
52
|
+
if (bytesRead === 0) {
|
|
53
|
+
return {
|
|
54
|
+
kind: "source",
|
|
55
|
+
source: Buffer.concat(chunks, totalBytes).toString("utf8")
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
chunks.push(buffer.subarray(0, bytesRead));
|
|
59
|
+
totalBytes += bytesRead;
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
kind: "invalid",
|
|
63
|
+
message: "Makefile exceeds the discovery size limit."
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return {
|
|
68
|
+
kind: "invalid",
|
|
69
|
+
message: "Makefile could not be read as a regular file."
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
await handle?.close().catch(() => undefined);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function literalTarget(source) {
|
|
77
|
+
if (source.includes("\0") || source.includes("\uFFFD")) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
const lines = source.split(/\r?\n/u);
|
|
81
|
+
if (lines.some((line) => /^test[ \t]*:(?![:=])/u.test(line))) {
|
|
82
|
+
return "test";
|
|
83
|
+
}
|
|
84
|
+
if (lines.some((line) => /^check[ \t]*:(?![:=])/u.test(line))) {
|
|
85
|
+
return "check";
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
function fileEvidence(target, detail) {
|
|
90
|
+
return {
|
|
91
|
+
kind: "file",
|
|
92
|
+
path: `Makefile#target:${target}`,
|
|
93
|
+
detail
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function userDecision(reason, message, evidence) {
|
|
97
|
+
return {
|
|
98
|
+
kind: "user-decision",
|
|
99
|
+
adapter: "make",
|
|
100
|
+
reason,
|
|
101
|
+
message,
|
|
102
|
+
evidence,
|
|
103
|
+
manualConfigAllowed: true
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function makeProposal(target, evidence) {
|
|
107
|
+
return {
|
|
108
|
+
id: `make:${target}`,
|
|
109
|
+
command: "make",
|
|
110
|
+
args: [target],
|
|
111
|
+
cwd: ".",
|
|
112
|
+
required: true,
|
|
113
|
+
evidence: target === "test"
|
|
114
|
+
? { kind: "test-count", minimum: 1 }
|
|
115
|
+
: { kind: "exit-code" },
|
|
116
|
+
sourceEvidence: evidence,
|
|
117
|
+
confidence: target === "test" ? "high" : "medium",
|
|
118
|
+
confirmed: false
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
export async function discoverMakeProject(root) {
|
|
122
|
+
const metadata = await readMakefile(path.join(root, "Makefile"));
|
|
123
|
+
if (metadata.kind === "missing") {
|
|
124
|
+
return {
|
|
125
|
+
kind: "no-match",
|
|
126
|
+
adapter: "make",
|
|
127
|
+
reason: "not-make-project",
|
|
128
|
+
message: "No Makefile was found.",
|
|
129
|
+
evidence: [],
|
|
130
|
+
manualConfigAllowed: true
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
const genericEvidence = [{
|
|
134
|
+
kind: "file",
|
|
135
|
+
path: "Makefile",
|
|
136
|
+
detail: "Found bounded Make metadata."
|
|
137
|
+
}];
|
|
138
|
+
if (metadata.kind === "invalid") {
|
|
139
|
+
return userDecision("invalid-manifest", metadata.message, genericEvidence);
|
|
140
|
+
}
|
|
141
|
+
if (metadata.source.includes("\0") || metadata.source.includes("\uFFFD")) {
|
|
142
|
+
return userDecision("invalid-manifest", "Makefile contains malformed text metadata.", genericEvidence);
|
|
143
|
+
}
|
|
144
|
+
const target = literalTarget(metadata.source);
|
|
145
|
+
if (target === null) {
|
|
146
|
+
return userDecision("no-known-commands", "No literal test or check target was found.", genericEvidence);
|
|
147
|
+
}
|
|
148
|
+
const proposalEvidence = [
|
|
149
|
+
fileEvidence(target, `Found literal ${target} target.`)
|
|
150
|
+
];
|
|
151
|
+
return {
|
|
152
|
+
kind: "proposals",
|
|
153
|
+
adapter: "make",
|
|
154
|
+
proposals: [makeProposal(target, proposalEvidence)],
|
|
155
|
+
evidence: proposalEvidence,
|
|
156
|
+
manualConfigAllowed: true
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
export const makeDiscoveryAdapter = {
|
|
160
|
+
id: "make",
|
|
161
|
+
discover: discoverMakeProject
|
|
162
|
+
};
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import { lstat, open } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
const LOCKFILES = [
|
|
5
|
+
{ path: "bun.lock", packageManager: "bun" },
|
|
6
|
+
{ path: "bun.lockb", packageManager: "bun" },
|
|
7
|
+
{ path: "npm-shrinkwrap.json", packageManager: "npm" },
|
|
8
|
+
{ path: "package-lock.json", packageManager: "npm" },
|
|
9
|
+
{ path: "pnpm-lock.yaml", packageManager: "pnpm" },
|
|
10
|
+
{ path: "yarn.lock", packageManager: "yarn" }
|
|
11
|
+
];
|
|
12
|
+
const MAX_PACKAGE_JSON_BYTES = 1024 * 1024;
|
|
13
|
+
function isRecord(value) {
|
|
14
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
15
|
+
}
|
|
16
|
+
function hasErrorCode(error, code) {
|
|
17
|
+
return (error instanceof Error &&
|
|
18
|
+
"code" in error &&
|
|
19
|
+
error.code === code);
|
|
20
|
+
}
|
|
21
|
+
async function fileExists(filePath) {
|
|
22
|
+
try {
|
|
23
|
+
const status = await lstat(filePath);
|
|
24
|
+
return status.isFile() && !status.isSymbolicLink();
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
if (hasErrorCode(error, "ENOENT")) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
async function readBoundedPackageJson(filePath) {
|
|
34
|
+
let handle;
|
|
35
|
+
try {
|
|
36
|
+
handle = await open(filePath, constants.O_RDONLY |
|
|
37
|
+
constants.O_NOFOLLOW |
|
|
38
|
+
constants.O_NONBLOCK);
|
|
39
|
+
const status = await handle.stat();
|
|
40
|
+
if (!status.isFile()) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
const chunks = [];
|
|
44
|
+
let totalBytes = 0;
|
|
45
|
+
while (totalBytes <= MAX_PACKAGE_JSON_BYTES) {
|
|
46
|
+
const remaining = MAX_PACKAGE_JSON_BYTES + 1 - totalBytes;
|
|
47
|
+
const buffer = Buffer.alloc(Math.min(64 * 1024, remaining));
|
|
48
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, null);
|
|
49
|
+
if (bytesRead === 0) {
|
|
50
|
+
return Buffer.concat(chunks, totalBytes).toString("utf8");
|
|
51
|
+
}
|
|
52
|
+
chunks.push(buffer.subarray(0, bytesRead));
|
|
53
|
+
totalBytes += bytesRead;
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
if (hasErrorCode(error, "ELOOP") ||
|
|
59
|
+
hasErrorCode(error, "EMLINK")) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
await handle?.close().catch(() => undefined);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function readPackageScripts(value) {
|
|
69
|
+
if (!isRecord(value) || !isRecord(value.scripts)) {
|
|
70
|
+
return {};
|
|
71
|
+
}
|
|
72
|
+
const scripts = {};
|
|
73
|
+
for (const [name, command] of Object.entries(value.scripts)) {
|
|
74
|
+
if (typeof command === "string" && command.trim().length > 0) {
|
|
75
|
+
scripts[name] = command;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return scripts;
|
|
79
|
+
}
|
|
80
|
+
function fileEvidence(filePath, detail) {
|
|
81
|
+
return { kind: "file", path: filePath, detail };
|
|
82
|
+
}
|
|
83
|
+
function userDecision(reason, message, evidence) {
|
|
84
|
+
return {
|
|
85
|
+
kind: "user-decision",
|
|
86
|
+
adapter: "node",
|
|
87
|
+
reason,
|
|
88
|
+
message,
|
|
89
|
+
evidence,
|
|
90
|
+
manualConfigAllowed: true
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function createTestProposal(packageManager, lockfileEvidence) {
|
|
94
|
+
const args = packageManager === "bun" ? ["run", "test"] : ["test"];
|
|
95
|
+
return {
|
|
96
|
+
id: "node:test",
|
|
97
|
+
command: packageManager,
|
|
98
|
+
args,
|
|
99
|
+
cwd: ".",
|
|
100
|
+
required: true,
|
|
101
|
+
evidence: { kind: "test-count", minimum: 1 },
|
|
102
|
+
sourceEvidence: [
|
|
103
|
+
...lockfileEvidence,
|
|
104
|
+
{
|
|
105
|
+
kind: "package-script",
|
|
106
|
+
path: "package.json#scripts.test",
|
|
107
|
+
detail: "Found an explicit test script."
|
|
108
|
+
}
|
|
109
|
+
],
|
|
110
|
+
confidence: "high",
|
|
111
|
+
confirmed: false
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
async function detectLockfiles(root) {
|
|
115
|
+
const detected = await Promise.all(LOCKFILES.map(async (definition) => (await fileExists(path.join(root, definition.path))
|
|
116
|
+
? definition
|
|
117
|
+
: null)));
|
|
118
|
+
return detected.filter((definition) => definition !== null);
|
|
119
|
+
}
|
|
120
|
+
export async function discoverNodeProject(root) {
|
|
121
|
+
const packageJsonPath = path.join(root, "package.json");
|
|
122
|
+
if (!await fileExists(packageJsonPath)) {
|
|
123
|
+
return {
|
|
124
|
+
kind: "no-match",
|
|
125
|
+
adapter: "node",
|
|
126
|
+
reason: "not-node-project",
|
|
127
|
+
message: "No package.json was found.",
|
|
128
|
+
evidence: [],
|
|
129
|
+
manualConfigAllowed: true
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
const packageEvidence = fileEvidence("package.json", "Found Node package metadata.");
|
|
133
|
+
let packageJson;
|
|
134
|
+
try {
|
|
135
|
+
const source = await readBoundedPackageJson(packageJsonPath);
|
|
136
|
+
if (source === null) {
|
|
137
|
+
return userDecision("invalid-package-json", "package.json is unsafe or exceeds the discovery size limit.", [packageEvidence]);
|
|
138
|
+
}
|
|
139
|
+
packageJson = JSON.parse(source);
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return userDecision("invalid-package-json", "package.json could not be parsed; choose verifier commands manually.", [packageEvidence]);
|
|
143
|
+
}
|
|
144
|
+
const detectedLockfiles = await detectLockfiles(root);
|
|
145
|
+
const lockfileEvidence = detectedLockfiles.map((definition) => fileEvidence(definition.path, `Detected ${definition.packageManager} lockfile.`));
|
|
146
|
+
const packageManagers = [
|
|
147
|
+
...new Set(detectedLockfiles.map((definition) => definition.packageManager))
|
|
148
|
+
];
|
|
149
|
+
if (packageManagers.length === 0) {
|
|
150
|
+
return userDecision("missing-lockfile", "No recognized package-manager lockfile was found.", [packageEvidence]);
|
|
151
|
+
}
|
|
152
|
+
if (packageManagers.length > 1) {
|
|
153
|
+
return userDecision("multiple-package-managers", "Multiple package managers were detected; select one explicitly.", lockfileEvidence);
|
|
154
|
+
}
|
|
155
|
+
const scripts = readPackageScripts(packageJson);
|
|
156
|
+
if (scripts.test === undefined) {
|
|
157
|
+
return userDecision("no-known-scripts", "No recognized verification script was found.", [packageEvidence, ...lockfileEvidence]);
|
|
158
|
+
}
|
|
159
|
+
const packageManager = packageManagers[0];
|
|
160
|
+
if (packageManager === undefined) {
|
|
161
|
+
return userDecision("missing-lockfile", "No recognized package-manager lockfile was found.", [packageEvidence]);
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
kind: "proposals",
|
|
165
|
+
adapter: "node",
|
|
166
|
+
packageManager,
|
|
167
|
+
proposals: [createTestProposal(packageManager, lockfileEvidence)],
|
|
168
|
+
evidence: [packageEvidence, ...lockfileEvidence],
|
|
169
|
+
manualConfigAllowed: true
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
export const nodeDiscoveryAdapter = {
|
|
173
|
+
id: "node",
|
|
174
|
+
discover: discoverNodeProject
|
|
175
|
+
};
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import { lstat, open } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
const MAX_PYPROJECT_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 readPyproject(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: "pyproject.toml could not be inspected safely."
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
if (status.isSymbolicLink() || !status.isFile()) {
|
|
25
|
+
return {
|
|
26
|
+
kind: "invalid",
|
|
27
|
+
message: "pyproject.toml must be a regular file."
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
if (status.size > MAX_PYPROJECT_BYTES) {
|
|
31
|
+
return {
|
|
32
|
+
kind: "invalid",
|
|
33
|
+
message: "pyproject.toml 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: "pyproject.toml must remain a regular file."
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
const chunks = [];
|
|
49
|
+
let totalBytes = 0;
|
|
50
|
+
while (totalBytes <= MAX_PYPROJECT_BYTES) {
|
|
51
|
+
const remaining = MAX_PYPROJECT_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: "pyproject.toml exceeds the discovery size limit."
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return {
|
|
70
|
+
kind: "invalid",
|
|
71
|
+
message: "pyproject.toml could not be read as a regular file."
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
await handle?.close().catch(() => undefined);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function hasMalformedTableHeader(source) {
|
|
79
|
+
if (source.includes("\0") || source.includes("\uFFFD")) {
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
for (const rawLine of source.split(/\r?\n/u)) {
|
|
83
|
+
const line = rawLine.trim();
|
|
84
|
+
if (!line.startsWith("[")) {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const tablePattern = line.startsWith("[[")
|
|
88
|
+
? /^\[\[[A-Za-z0-9_.-]+\]\](?:\s+#.*)?$/u
|
|
89
|
+
: /^\[[A-Za-z0-9_.-]+\](?:\s+#.*)?$/u;
|
|
90
|
+
if (!tablePattern.test(line)) {
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
function hasExplicitPytestEvidence(source) {
|
|
97
|
+
return source.split(/\r?\n/u).some((line) => (/^\s*\[tool\.pytest\.ini_options\]\s*(?:#.*)?$/u.test(line)));
|
|
98
|
+
}
|
|
99
|
+
function fileEvidence(detail) {
|
|
100
|
+
return { kind: "file", path: "pyproject.toml", detail };
|
|
101
|
+
}
|
|
102
|
+
function userDecision(reason, message, evidence) {
|
|
103
|
+
return {
|
|
104
|
+
kind: "user-decision",
|
|
105
|
+
adapter: "python",
|
|
106
|
+
reason,
|
|
107
|
+
message,
|
|
108
|
+
evidence,
|
|
109
|
+
manualConfigAllowed: true
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function pytestProposal(evidence) {
|
|
113
|
+
return {
|
|
114
|
+
id: "python:pytest",
|
|
115
|
+
command: "python",
|
|
116
|
+
args: ["-m", "pytest"],
|
|
117
|
+
cwd: ".",
|
|
118
|
+
required: true,
|
|
119
|
+
evidence: { kind: "test-count", minimum: 1 },
|
|
120
|
+
sourceEvidence: evidence,
|
|
121
|
+
confidence: "high",
|
|
122
|
+
confirmed: false
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
export async function discoverPythonProject(root) {
|
|
126
|
+
const metadata = await readPyproject(path.join(root, "pyproject.toml"));
|
|
127
|
+
if (metadata.kind === "missing") {
|
|
128
|
+
return {
|
|
129
|
+
kind: "no-match",
|
|
130
|
+
adapter: "python",
|
|
131
|
+
reason: "not-python-project",
|
|
132
|
+
message: "No pyproject.toml was found.",
|
|
133
|
+
evidence: [],
|
|
134
|
+
manualConfigAllowed: true
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
const evidence = [
|
|
138
|
+
fileEvidence("Found bounded Python project metadata.")
|
|
139
|
+
];
|
|
140
|
+
if (metadata.kind === "invalid") {
|
|
141
|
+
return userDecision("invalid-manifest", metadata.message, evidence);
|
|
142
|
+
}
|
|
143
|
+
if (hasMalformedTableHeader(metadata.source)) {
|
|
144
|
+
return userDecision("invalid-manifest", "pyproject.toml contains malformed table metadata.", evidence);
|
|
145
|
+
}
|
|
146
|
+
if (!hasExplicitPytestEvidence(metadata.source)) {
|
|
147
|
+
return userDecision("no-known-commands", "No explicit pytest configuration was found.", evidence);
|
|
148
|
+
}
|
|
149
|
+
const proposalEvidence = [
|
|
150
|
+
fileEvidence("Found explicit [tool.pytest.ini_options] evidence.")
|
|
151
|
+
];
|
|
152
|
+
return {
|
|
153
|
+
kind: "proposals",
|
|
154
|
+
adapter: "python",
|
|
155
|
+
proposals: [pytestProposal(proposalEvidence)],
|
|
156
|
+
evidence: proposalEvidence,
|
|
157
|
+
manualConfigAllowed: true
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
export const pythonDiscoveryAdapter = {
|
|
161
|
+
id: "python",
|
|
162
|
+
discover: discoverPythonProject
|
|
163
|
+
};
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import { lstat, open } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
const MAX_CARGO_TOML_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 readCargoToml(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: "Cargo.toml could not be inspected safely."
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
if (status.isSymbolicLink() || !status.isFile()) {
|
|
25
|
+
return {
|
|
26
|
+
kind: "invalid",
|
|
27
|
+
message: "Cargo.toml must be a regular file."
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
if (status.size > MAX_CARGO_TOML_BYTES) {
|
|
31
|
+
return {
|
|
32
|
+
kind: "invalid",
|
|
33
|
+
message: "Cargo.toml exceeds the discovery size limit."
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
let handle;
|
|
37
|
+
try {
|
|
38
|
+
handle = await open(filePath, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
39
|
+
const openedStatus = await handle.stat();
|
|
40
|
+
if (!openedStatus.isFile()) {
|
|
41
|
+
return {
|
|
42
|
+
kind: "invalid",
|
|
43
|
+
message: "Cargo.toml must remain a regular file."
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const chunks = [];
|
|
47
|
+
let totalBytes = 0;
|
|
48
|
+
while (totalBytes <= MAX_CARGO_TOML_BYTES) {
|
|
49
|
+
const remaining = MAX_CARGO_TOML_BYTES + 1 - totalBytes;
|
|
50
|
+
const buffer = Buffer.alloc(Math.min(64 * 1024, remaining));
|
|
51
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, null);
|
|
52
|
+
if (bytesRead === 0) {
|
|
53
|
+
return {
|
|
54
|
+
kind: "source",
|
|
55
|
+
source: Buffer.concat(chunks, totalBytes).toString("utf8")
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
chunks.push(buffer.subarray(0, bytesRead));
|
|
59
|
+
totalBytes += bytesRead;
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
kind: "invalid",
|
|
63
|
+
message: "Cargo.toml exceeds the discovery size limit."
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return {
|
|
68
|
+
kind: "invalid",
|
|
69
|
+
message: "Cargo.toml could not be read as a regular file."
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
await handle?.close().catch(() => undefined);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function hasMalformedTableHeader(source) {
|
|
77
|
+
if (source.includes("\0") || source.includes("\uFFFD")) {
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
for (const rawLine of source.split(/\r?\n/u)) {
|
|
81
|
+
const line = rawLine.trim();
|
|
82
|
+
if (!line.startsWith("[")) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const tablePattern = line.startsWith("[[")
|
|
86
|
+
? /^\[\[[A-Za-z0-9_.-]+\]\](?:\s+#.*)?$/u
|
|
87
|
+
: /^\[[A-Za-z0-9_.-]+\](?:\s+#.*)?$/u;
|
|
88
|
+
if (!tablePattern.test(line)) {
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
function hasCargoRootEvidence(source) {
|
|
95
|
+
return source.split(/\r?\n/u).some((line) => (/^\s*\[(?:package|workspace)\]\s*(?:#.*)?$/u.test(line)));
|
|
96
|
+
}
|
|
97
|
+
function fileEvidence(detail) {
|
|
98
|
+
return { kind: "file", path: "Cargo.toml", detail };
|
|
99
|
+
}
|
|
100
|
+
function userDecision(reason, message, evidence) {
|
|
101
|
+
return {
|
|
102
|
+
kind: "user-decision",
|
|
103
|
+
adapter: "rust",
|
|
104
|
+
reason,
|
|
105
|
+
message,
|
|
106
|
+
evidence,
|
|
107
|
+
manualConfigAllowed: true
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function cargoTestProposal(evidence) {
|
|
111
|
+
return {
|
|
112
|
+
id: "rust:cargo-test",
|
|
113
|
+
command: "cargo",
|
|
114
|
+
args: ["test"],
|
|
115
|
+
cwd: ".",
|
|
116
|
+
required: true,
|
|
117
|
+
evidence: { kind: "test-count", minimum: 1 },
|
|
118
|
+
sourceEvidence: evidence,
|
|
119
|
+
confidence: "high",
|
|
120
|
+
confirmed: false
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
export async function discoverRustProject(root) {
|
|
124
|
+
const metadata = await readCargoToml(path.join(root, "Cargo.toml"));
|
|
125
|
+
if (metadata.kind === "missing") {
|
|
126
|
+
return {
|
|
127
|
+
kind: "no-match",
|
|
128
|
+
adapter: "rust",
|
|
129
|
+
reason: "not-rust-project",
|
|
130
|
+
message: "No Cargo.toml was found.",
|
|
131
|
+
evidence: [],
|
|
132
|
+
manualConfigAllowed: true
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
const evidence = [fileEvidence("Found bounded Cargo metadata.")];
|
|
136
|
+
if (metadata.kind === "invalid") {
|
|
137
|
+
return userDecision("invalid-manifest", metadata.message, evidence);
|
|
138
|
+
}
|
|
139
|
+
if (hasMalformedTableHeader(metadata.source)) {
|
|
140
|
+
return userDecision("invalid-manifest", "Cargo.toml contains malformed table metadata.", evidence);
|
|
141
|
+
}
|
|
142
|
+
if (!hasCargoRootEvidence(metadata.source)) {
|
|
143
|
+
return userDecision("no-known-commands", "No explicit Cargo package or workspace was found.", evidence);
|
|
144
|
+
}
|
|
145
|
+
const proposalEvidence = [
|
|
146
|
+
fileEvidence("Found explicit Cargo package or workspace evidence.")
|
|
147
|
+
];
|
|
148
|
+
return {
|
|
149
|
+
kind: "proposals",
|
|
150
|
+
adapter: "rust",
|
|
151
|
+
proposals: [cargoTestProposal(proposalEvidence)],
|
|
152
|
+
evidence: proposalEvidence,
|
|
153
|
+
manualConfigAllowed: true
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
export const rustDiscoveryAdapter = {
|
|
157
|
+
id: "rust",
|
|
158
|
+
discover: discoverRustProject
|
|
159
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|