@kylecheng3146/agent-ops 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +88 -13
- package/dist/packages/cli/src/args.js +39 -6
- package/dist/packages/cli/src/bin.js +33 -11
- package/dist/packages/cli/src/cli.js +8 -8
- package/dist/packages/cli/src/commands/doctor.js +31 -9
- package/dist/packages/cli/src/commands/hook.js +18 -16
- package/dist/packages/cli/src/commands/init.js +29 -7
- package/dist/packages/cli/src/commands/review.js +5 -1
- package/dist/packages/cli/src/commands/uninstall.js +6 -5
- package/dist/packages/cli/src/commands/update.js +13 -5
- package/dist/packages/cli/src/context.js +9 -3
- package/dist/packages/cli/src/hook-process.js +109 -7
- package/dist/packages/cli/src/plan-output.js +9 -4
- package/dist/packages/cli/src/public-plan.js +62 -0
- package/dist/packages/cli/src/ui.js +242 -1
- package/dist/packages/cli/src/version.js +1 -1
- package/dist/packages/cli/src/wizard.js +68 -2
- package/dist/runtime/src/adapters/claude/config.js +0 -8
- package/dist/runtime/src/adapters/claude/events.js +26 -0
- package/dist/runtime/src/adapters/claude/output.js +6 -6
- package/dist/runtime/src/adapters/claude/surfaces.js +70 -0
- package/dist/runtime/src/adapters/codex/config.js +29 -11
- package/dist/runtime/src/adapters/codex/events.js +26 -0
- package/dist/runtime/src/adapters/codex/output.js +10 -0
- package/dist/runtime/src/adapters/codex/surfaces.js +12 -0
- package/dist/runtime/src/adapters/opencode/config.js +170 -0
- package/dist/runtime/src/adapters/opencode/events.js +49 -0
- package/dist/runtime/src/adapters/opencode/input.js +32 -0
- package/dist/runtime/src/adapters/opencode/output.js +23 -0
- package/dist/runtime/src/adapters/opencode/surfaces.js +23 -0
- package/dist/runtime/src/config/explain.js +7 -0
- package/dist/runtime/src/config/hash.js +24 -0
- package/dist/runtime/src/config/merge.js +6 -2
- package/dist/runtime/src/config/migrate.js +19 -4
- package/dist/runtime/src/contracts.js +10 -1
- package/dist/runtime/src/fs/manifest.js +32 -1
- package/dist/runtime/src/fs/transaction.js +14 -2
- package/dist/runtime/src/hooks/advisory.js +16 -0
- package/dist/runtime/src/hooks/stop-service.js +70 -0
- package/dist/runtime/src/hooks/stop-verify.js +4 -1
- package/dist/runtime/src/install/doctor.js +119 -9
- package/dist/runtime/src/install/harness.js +296 -35
- package/dist/runtime/src/install/hooks.js +22 -17
- package/dist/runtime/src/install/ownership.js +110 -34
- package/dist/runtime/src/install/plan.js +207 -28
- package/dist/runtime/src/install/probes.js +9 -43
- package/dist/runtime/src/install/profiles.js +8 -1
- package/dist/runtime/src/install/surface-inspection.js +296 -0
- package/dist/runtime/src/install/surfaces.js +11 -0
- package/dist/runtime/src/install/uninstall.js +10 -3
- package/dist/runtime/src/install/update.js +8 -2
- package/dist/runtime/src/schema/validate.js +37 -18
- package/dist/runtime/src/task/service.js +3 -3
- package/dist/runtime/src/task/store.js +12 -3
- package/dist/runtime/src/verify/command-executor.js +113 -0
- package/dist/runtime/src/verify/evidence.js +4 -24
- package/dist/runtime/src/verify/service.js +20 -92
- package/dist/runtime/src/verify/spawn.js +6 -1
- package/docs/en/guides/configuration.md +83 -0
- package/docs/en/guides/quickstart.md +9 -0
- package/docs/en/guides/security.md +5 -0
- package/docs/en/spec/README.md +9 -0
- package/docs/en/spec/harness-adapters.md +66 -2
- package/docs/en/spec/maintenance.md +11 -0
- package/docs/en/spec/review.md +11 -0
- package/docs/zh-TW/guides/configuration.md +77 -0
- package/docs/zh-TW/guides/quickstart.md +9 -0
- package/docs/zh-TW/guides/security.md +5 -0
- package/docs/zh-TW/spec/README.md +9 -0
- package/docs/zh-TW/spec/harness-adapters.md +57 -3
- package/docs/zh-TW/spec/maintenance.md +11 -1
- package/docs/zh-TW/spec/review.md +10 -0
- package/package.json +8 -4
- package/postinstall.cjs +102 -0
- package/schemas/config.schema.json +55 -1
- package/schemas/manifest.schema.json +7 -2
- package/templates/common/AGENTS.block.md +2 -1
- package/templates/common/CLAUDE.block.md +2 -1
- package/dist/runtime/src/review/claude-runner.js +0 -4
- package/dist/runtime/src/review/codex-runner.js +0 -4
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { opencodePluginTarget } from "./config.js";
|
|
2
|
+
export const OPENCODE_SURFACE_IDS = {
|
|
3
|
+
plugin: "opencode-plugin",
|
|
4
|
+
config: "opencode-config"
|
|
5
|
+
};
|
|
6
|
+
export function opencodeSurfaces(scope, root) {
|
|
7
|
+
return [
|
|
8
|
+
{
|
|
9
|
+
id: OPENCODE_SURFACE_IDS.plugin,
|
|
10
|
+
path: opencodePluginTarget(scope, root).path,
|
|
11
|
+
scope,
|
|
12
|
+
access: "managed-default",
|
|
13
|
+
representation: "javascript"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
id: OPENCODE_SURFACE_IDS.config,
|
|
17
|
+
path: "opencode.json",
|
|
18
|
+
scope,
|
|
19
|
+
access: "inspect-only",
|
|
20
|
+
representation: "json"
|
|
21
|
+
}
|
|
22
|
+
];
|
|
23
|
+
}
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
export function explainConfig(merged) {
|
|
2
2
|
return {
|
|
3
3
|
schemaVersion: merged.config.schemaVersion,
|
|
4
|
+
features: {
|
|
5
|
+
stopVerification: {
|
|
6
|
+
enabled: merged.config.features.stopVerification.enabled,
|
|
7
|
+
source: merged.provenance.features.source,
|
|
8
|
+
sourcePath: merged.provenance.features.sourcePath
|
|
9
|
+
}
|
|
10
|
+
},
|
|
4
11
|
profiles: merged.provenance.profiles.map((entry) => ({
|
|
5
12
|
id: entry.value,
|
|
6
13
|
source: entry.source,
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { sha256 } from "../fs/hash.js";
|
|
2
|
+
import { AgentOpsError } from "../fs/paths.js";
|
|
3
|
+
function canonicalJson(value) {
|
|
4
|
+
if (value === null ||
|
|
5
|
+
typeof value === "boolean" ||
|
|
6
|
+
typeof value === "number" ||
|
|
7
|
+
typeof value === "string") {
|
|
8
|
+
return JSON.stringify(value);
|
|
9
|
+
}
|
|
10
|
+
if (Array.isArray(value)) {
|
|
11
|
+
return `[${value.map(canonicalJson).join(",")}]`;
|
|
12
|
+
}
|
|
13
|
+
if (typeof value === "object") {
|
|
14
|
+
const record = value;
|
|
15
|
+
return `{${Object.keys(record)
|
|
16
|
+
.sort()
|
|
17
|
+
.map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`)
|
|
18
|
+
.join(",")}}`;
|
|
19
|
+
}
|
|
20
|
+
throw new AgentOpsError("CONFIG_HASH_INVALID", "Configuration contains an unsupported value.");
|
|
21
|
+
}
|
|
22
|
+
export function calculateConfigHash(config) {
|
|
23
|
+
return sha256(canonicalJson(config));
|
|
24
|
+
}
|
|
@@ -108,8 +108,10 @@ export function mergeConfigLayers(inputLayers) {
|
|
|
108
108
|
const mappings = new Map();
|
|
109
109
|
const exceptions = new Map();
|
|
110
110
|
let schemaVersion;
|
|
111
|
+
let features;
|
|
111
112
|
for (const layer of layers) {
|
|
112
113
|
schemaVersion = effective(layer.config.schemaVersion, layer);
|
|
114
|
+
features = effective(layer.config.features, layer);
|
|
113
115
|
for (const profile of layer.config.profiles) {
|
|
114
116
|
profiles.set(profile, effective(profile, layer));
|
|
115
117
|
}
|
|
@@ -139,22 +141,24 @@ export function mergeConfigLayers(inputLayers) {
|
|
|
139
141
|
exceptions.set(key, effective(securityException, layer));
|
|
140
142
|
}
|
|
141
143
|
}
|
|
142
|
-
if (schemaVersion === undefined) {
|
|
144
|
+
if (schemaVersion === undefined || features === undefined) {
|
|
143
145
|
throw new AgentOpsError("CONFIG_LAYER_REQUIRED", "At least one configuration layer is required.");
|
|
144
146
|
}
|
|
145
147
|
const provenance = {
|
|
146
148
|
schemaVersion,
|
|
149
|
+
features,
|
|
147
150
|
profiles: [...profiles.values()],
|
|
148
151
|
verificationCommands: [...commands.values()],
|
|
149
152
|
pathMappings: [...mappings.values()],
|
|
150
153
|
securityExceptions: [...exceptions.values()]
|
|
151
154
|
};
|
|
152
155
|
const config = {
|
|
153
|
-
schemaVersion:
|
|
156
|
+
schemaVersion: schemaVersion.value,
|
|
154
157
|
profiles: provenance.profiles.map(({ value }) => value),
|
|
155
158
|
verification: {
|
|
156
159
|
commands: provenance.verificationCommands.map(({ value }) => value)
|
|
157
160
|
},
|
|
161
|
+
features: provenance.features.value,
|
|
158
162
|
pathMappings: provenance.pathMappings.map(({ value }) => value),
|
|
159
163
|
securityExceptions: provenance.securityExceptions.map(({ value }) => value)
|
|
160
164
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { CONFIG_SCHEMA_VERSION } from "../contracts.js";
|
|
2
2
|
import { AgentOpsError } from "../fs/paths.js";
|
|
3
3
|
import { validateConfig } from "../schema/validate.js";
|
|
4
4
|
function isRecord(value) {
|
|
@@ -18,6 +18,21 @@ const MIGRATIONS = new Map([
|
|
|
18
18
|
verification: { commands: clone(commands) }
|
|
19
19
|
};
|
|
20
20
|
}
|
|
21
|
+
],
|
|
22
|
+
[
|
|
23
|
+
1,
|
|
24
|
+
(input) => {
|
|
25
|
+
const { schemaVersion: _schemaVersion, ...rest } = input;
|
|
26
|
+
return {
|
|
27
|
+
...rest,
|
|
28
|
+
schemaVersion: CONFIG_SCHEMA_VERSION,
|
|
29
|
+
features: {
|
|
30
|
+
stopVerification: {
|
|
31
|
+
enabled: false
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
}
|
|
21
36
|
]
|
|
22
37
|
]);
|
|
23
38
|
function schemaVersionOf(value) {
|
|
@@ -30,13 +45,13 @@ function schemaVersionOf(value) {
|
|
|
30
45
|
}
|
|
31
46
|
export function previewConfigMigration(input) {
|
|
32
47
|
const sourceVersion = schemaVersionOf(input);
|
|
33
|
-
if (sourceVersion >
|
|
34
|
-
throw new AgentOpsError("CONFIG_SCHEMA_FUTURE", `Config schemaVersion ${sourceVersion} is newer than supported version ${
|
|
48
|
+
if (sourceVersion > CONFIG_SCHEMA_VERSION) {
|
|
49
|
+
throw new AgentOpsError("CONFIG_SCHEMA_FUTURE", `Config schemaVersion ${sourceVersion} is newer than supported version ${CONFIG_SCHEMA_VERSION}.`);
|
|
35
50
|
}
|
|
36
51
|
let migrated = clone(input);
|
|
37
52
|
let version = sourceVersion;
|
|
38
53
|
const steps = [];
|
|
39
|
-
while (version <
|
|
54
|
+
while (version < CONFIG_SCHEMA_VERSION) {
|
|
40
55
|
const migration = MIGRATIONS.get(version);
|
|
41
56
|
if (migration === undefined || !isRecord(migrated)) {
|
|
42
57
|
throw new AgentOpsError("CONFIG_MIGRATION_MISSING", `No migration is registered from config schemaVersion ${version}.`);
|
|
@@ -1 +1,10 @@
|
|
|
1
|
-
export const
|
|
1
|
+
export const CONFIG_SCHEMA_VERSION = 2;
|
|
2
|
+
export const TASK_SCHEMA_VERSION = 1;
|
|
3
|
+
export const EVIDENCE_SCHEMA_VERSION = 1;
|
|
4
|
+
/** @deprecated Use the document-specific schema version constants. */
|
|
5
|
+
export const SCHEMA_VERSION = CONFIG_SCHEMA_VERSION;
|
|
6
|
+
/**
|
|
7
|
+
* The manifest versions independently of config, tasks, and evidence: only its
|
|
8
|
+
* own shape changed when the harness selection became a list.
|
|
9
|
+
*/
|
|
10
|
+
export const MANIFEST_SCHEMA_VERSION = 2;
|
|
@@ -1,6 +1,37 @@
|
|
|
1
|
+
import { MANIFEST_SCHEMA_VERSION } from "../contracts.js";
|
|
1
2
|
import { validateManifest } from "../schema/validate.js";
|
|
2
3
|
import { AgentOpsError } from "./paths.js";
|
|
3
4
|
export const PROJECT_MANIFEST_PATH = ".agent-ops/manifest.json";
|
|
5
|
+
const LEGACY_HARNESS_SELECTIONS = {
|
|
6
|
+
both: ["codex", "claude"],
|
|
7
|
+
claude: ["claude"],
|
|
8
|
+
codex: ["codex"]
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Manifests written up to 0.1.4 stored a single harness string, with `"both"`
|
|
12
|
+
* standing in for two harnesses. Reading one upgrades it in memory; the file is
|
|
13
|
+
* rewritten the next time `update` runs.
|
|
14
|
+
*/
|
|
15
|
+
function migrateLegacyManifest(value) {
|
|
16
|
+
if (typeof value !== "object" ||
|
|
17
|
+
value === null ||
|
|
18
|
+
Array.isArray(value)) {
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
const record = value;
|
|
22
|
+
if (record.schemaVersion !== 1 || typeof record.harness !== "string") {
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
const harness = LEGACY_HARNESS_SELECTIONS[record.harness];
|
|
26
|
+
if (harness === undefined) {
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
...record,
|
|
31
|
+
schemaVersion: MANIFEST_SCHEMA_VERSION,
|
|
32
|
+
harness
|
|
33
|
+
};
|
|
34
|
+
}
|
|
4
35
|
export function parseInstallManifest(source) {
|
|
5
36
|
let value;
|
|
6
37
|
try {
|
|
@@ -9,7 +40,7 @@ export function parseInstallManifest(source) {
|
|
|
9
40
|
catch (error) {
|
|
10
41
|
throw new AgentOpsError("MANIFEST_INVALID_JSON", "The installation manifest is not valid JSON.", { cause: error });
|
|
11
42
|
}
|
|
12
|
-
const result = validateManifest(value);
|
|
43
|
+
const result = validateManifest(migrateLegacyManifest(value));
|
|
13
44
|
if (!result.ok) {
|
|
14
45
|
throw new AgentOpsError("MANIFEST_INVALID", `${result.errors[0]?.path ?? "$"}: ${result.errors[0]?.message ?? "Invalid installation manifest."}`);
|
|
15
46
|
}
|
|
@@ -147,13 +147,25 @@ function assertTransactionPlan(plan) {
|
|
|
147
147
|
}
|
|
148
148
|
if (operation.kind === "write") {
|
|
149
149
|
if (typeof operation.content !== "string" ||
|
|
150
|
-
|
|
150
|
+
(operation.disclosure !== undefined &&
|
|
151
|
+
operation.disclosure !== "full" &&
|
|
152
|
+
operation.disclosure !== "opaque") ||
|
|
153
|
+
!hasOnlyKeys(operation, [
|
|
154
|
+
"kind",
|
|
155
|
+
"path",
|
|
156
|
+
"content",
|
|
157
|
+
"expectedHash",
|
|
158
|
+
"disclosure"
|
|
159
|
+
])) {
|
|
151
160
|
throw new AgentOpsError("INVALID_TRANSACTION_PLAN", `Invalid write operation at index ${index}.`);
|
|
152
161
|
}
|
|
153
162
|
continue;
|
|
154
163
|
}
|
|
155
164
|
if (operation.kind !== "remove" ||
|
|
156
|
-
|
|
165
|
+
(operation.disclosure !== undefined &&
|
|
166
|
+
operation.disclosure !== "full" &&
|
|
167
|
+
operation.disclosure !== "opaque") ||
|
|
168
|
+
!hasOnlyKeys(operation, ["kind", "path", "expectedHash", "disclosure"])) {
|
|
157
169
|
throw new AgentOpsError("INVALID_TRANSACTION_PLAN", `Invalid remove operation at index ${index}.`);
|
|
158
170
|
}
|
|
159
171
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { appendLocalLog } from "../logging/local-log.js";
|
|
4
|
+
import { localStatePaths } from "../security/permissions.js";
|
|
5
|
+
/**
|
|
6
|
+
* Records the bounded lifecycle summary signal locally. It deliberately does
|
|
7
|
+
* not include project paths, prompts, commands, or verification evidence.
|
|
8
|
+
*/
|
|
9
|
+
export async function runLifecycleAdvisory(_event, options = {}) {
|
|
10
|
+
const home = options.homeDirectory ?? process.env.AGENT_OPS_HOME ?? homedir();
|
|
11
|
+
const state = localStatePaths(home);
|
|
12
|
+
await appendLocalLog(join(state.logDirectory, "events.ndjson"), { type: "diagnostic", code: "LIFECYCLE_SUMMARY" }, {
|
|
13
|
+
anchorDirectory: state.anchorDirectory,
|
|
14
|
+
...(options.now === undefined ? {} : { now: options.now })
|
|
15
|
+
});
|
|
16
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { calculateConfigHash } from "../config/hash.js";
|
|
2
|
+
import { AgentOpsError, resolveContainedPath } from "../fs/paths.js";
|
|
3
|
+
import { aggregateVerificationStatus, executeConfiguredCommand } from "../verify/command-executor.js";
|
|
4
|
+
import { collectChangeSurface } from "../verify/change-surface.js";
|
|
5
|
+
import { selectVerificationScope } from "../verify/scope.js";
|
|
6
|
+
export const STOP_VERIFICATION_ENV = "AGENT_OPS_STOP_VERIFY_ACTIVE";
|
|
7
|
+
const HASH_PATTERN = /^[a-f0-9]{64}$/;
|
|
8
|
+
function stopError(code, message) {
|
|
9
|
+
return new AgentOpsError(code, message);
|
|
10
|
+
}
|
|
11
|
+
function commandById(config, commandId) {
|
|
12
|
+
const command = config.verification.commands.find((candidate) => candidate.id === commandId);
|
|
13
|
+
if (command === undefined) {
|
|
14
|
+
throw stopError("STOP_VERIFICATION_COMMAND_NOT_FOUND", `Stop verification command not found: ${commandId}`);
|
|
15
|
+
}
|
|
16
|
+
return command;
|
|
17
|
+
}
|
|
18
|
+
export class StopVerificationService {
|
|
19
|
+
#options;
|
|
20
|
+
constructor(options) {
|
|
21
|
+
this.#options = options;
|
|
22
|
+
}
|
|
23
|
+
#assertReady() {
|
|
24
|
+
if (!this.#options.config.features.stopVerification.enabled) {
|
|
25
|
+
throw stopError("STOP_VERIFICATION_DISABLED", "Stop verification is not enabled in configuration.");
|
|
26
|
+
}
|
|
27
|
+
if (this.#options.config.verification.commands.length === 0) {
|
|
28
|
+
throw stopError("STOP_VERIFICATION_COMMANDS_REQUIRED", "Stop verification requires at least one configured command.");
|
|
29
|
+
}
|
|
30
|
+
if (!this.#options.trusted) {
|
|
31
|
+
throw stopError("STOP_VERIFICATION_UNTRUSTED", "Stop verification requires a trusted repository.");
|
|
32
|
+
}
|
|
33
|
+
if (this.#options.recursionActive ||
|
|
34
|
+
process.env[STOP_VERIFICATION_ENV] === "1") {
|
|
35
|
+
throw stopError("STOP_VERIFICATION_RECURSION", "Stop verification recursion is active.");
|
|
36
|
+
}
|
|
37
|
+
if (!HASH_PATTERN.test(this.#options.configHash) ||
|
|
38
|
+
calculateConfigHash(this.#options.config) !== this.#options.configHash) {
|
|
39
|
+
throw stopError("STOP_VERIFICATION_UNCONFIRMED", "Stop verification configuration is stale or unconfirmed.");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async verify() {
|
|
43
|
+
this.#assertReady();
|
|
44
|
+
const surface = await collectChangeSurface(this.#options.gitRunner);
|
|
45
|
+
const selection = selectVerificationScope(surface.paths, this.#options.config);
|
|
46
|
+
const executions = [];
|
|
47
|
+
for (const commandId of selection.verifierIds) {
|
|
48
|
+
const command = commandById(this.#options.config, commandId);
|
|
49
|
+
const cwd = command.cwd === "."
|
|
50
|
+
? this.#options.root
|
|
51
|
+
: await resolveContainedPath(this.#options.root, command.cwd);
|
|
52
|
+
const result = await executeConfiguredCommand(command, {
|
|
53
|
+
cwd,
|
|
54
|
+
runner: this.#options.processRunner,
|
|
55
|
+
trusted: true,
|
|
56
|
+
env: { [STOP_VERIFICATION_ENV]: "1" }
|
|
57
|
+
});
|
|
58
|
+
executions.push(result);
|
|
59
|
+
}
|
|
60
|
+
const results = executions.map(({ commandId, exitCode, testCount }) => ({
|
|
61
|
+
commandId,
|
|
62
|
+
exitCode,
|
|
63
|
+
testCount
|
|
64
|
+
}));
|
|
65
|
+
return {
|
|
66
|
+
status: aggregateVerificationStatus(executions),
|
|
67
|
+
results
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -16,11 +16,14 @@ function boundedEvidence(results) {
|
|
|
16
16
|
(!Number.isSafeInteger(result.testCount) || result.testCount < 0)))) {
|
|
17
17
|
return null;
|
|
18
18
|
}
|
|
19
|
-
|
|
19
|
+
const bounded = results.map(({ commandId, exitCode, testCount }) => ({
|
|
20
20
|
commandId,
|
|
21
21
|
exitCode,
|
|
22
22
|
testCount
|
|
23
23
|
}));
|
|
24
|
+
return Buffer.byteLength(JSON.stringify(bounded), "utf8") <= 4096
|
|
25
|
+
? bounded
|
|
26
|
+
: null;
|
|
24
27
|
}
|
|
25
28
|
function evidenceSupportsStatus(status, results) {
|
|
26
29
|
if (status !== "PASS") {
|
|
@@ -5,11 +5,17 @@ import { parseInstallManifest, PROJECT_MANIFEST_PATH } from "../fs/manifest.js";
|
|
|
5
5
|
import { resolveContainedPath } from "../fs/paths.js";
|
|
6
6
|
import { validateConfig } from "../schema/validate.js";
|
|
7
7
|
import { assertExpectedManagedBlock, assertSupportedManifestOwnership } from "./ownership.js";
|
|
8
|
+
import { isOpencodeManagedPlugin } from "../adapters/opencode/config.js";
|
|
9
|
+
import { harnessDescriptor } from "./harness.js";
|
|
10
|
+
import { resolveProfiles } from "./profiles.js";
|
|
11
|
+
import { inspectHarnessSurfaces, inspectHarnessRegistrations } from "./surface-inspection.js";
|
|
8
12
|
const CONFIG_PATH = ".agent-ops/config.json";
|
|
9
13
|
const MINIMUM_NODE_VERSION = [22, 14, 0];
|
|
10
14
|
const MAX_DOCTOR_FILE_BYTES = 1024 * 1024;
|
|
11
|
-
function check(id, status, message) {
|
|
12
|
-
return
|
|
15
|
+
function check(id, status, message, code) {
|
|
16
|
+
return code === undefined
|
|
17
|
+
? { id, status, message }
|
|
18
|
+
: { id, status, message, code };
|
|
13
19
|
}
|
|
14
20
|
function parseNodeVersion(version) {
|
|
15
21
|
const match = /^v?(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/.exec(version);
|
|
@@ -80,7 +86,7 @@ async function readContainedText(root, path) {
|
|
|
80
86
|
async function checkManifest(root) {
|
|
81
87
|
try {
|
|
82
88
|
const manifest = parseInstallManifest(await readContainedText(root, PROJECT_MANIFEST_PATH));
|
|
83
|
-
assertSupportedManifestOwnership(manifest);
|
|
89
|
+
assertSupportedManifestOwnership(manifest, root);
|
|
84
90
|
return {
|
|
85
91
|
check: check("manifest", "PASS", "Installation manifest is valid."),
|
|
86
92
|
manifest
|
|
@@ -124,7 +130,9 @@ async function checkArtifacts(root, manifest) {
|
|
|
124
130
|
for (const artifact of manifest.artifacts) {
|
|
125
131
|
try {
|
|
126
132
|
const content = await readContained(root, artifact.path);
|
|
127
|
-
if (sha256(content) !== artifact.hash
|
|
133
|
+
if (sha256(content) !== artifact.hash ||
|
|
134
|
+
(artifact.id === "opencode-plugin" &&
|
|
135
|
+
!isOpencodeManagedPlugin(content.toString("utf8")))) {
|
|
128
136
|
failures.push(artifact.path);
|
|
129
137
|
}
|
|
130
138
|
}
|
|
@@ -141,7 +149,8 @@ async function checkMarkers(root, manifest) {
|
|
|
141
149
|
return check("markers", "FAIL", "Managed blocks cannot be verified without a valid manifest.");
|
|
142
150
|
}
|
|
143
151
|
const failures = [];
|
|
144
|
-
const
|
|
152
|
+
const legacyPaths = [];
|
|
153
|
+
const expectedMarkers = assertSupportedManifestOwnership(manifest, root);
|
|
145
154
|
for (const marker of manifest.markers) {
|
|
146
155
|
try {
|
|
147
156
|
const source = await readContainedText(root, marker.path);
|
|
@@ -150,15 +159,22 @@ async function checkMarkers(root, manifest) {
|
|
|
150
159
|
failures.push(marker.path);
|
|
151
160
|
continue;
|
|
152
161
|
}
|
|
153
|
-
assertExpectedManagedBlock(source, marker, expected);
|
|
162
|
+
const match = assertExpectedManagedBlock(source, marker, expected);
|
|
163
|
+
if (match === "legacy") {
|
|
164
|
+
legacyPaths.push(marker.path);
|
|
165
|
+
}
|
|
154
166
|
}
|
|
155
167
|
catch {
|
|
156
168
|
failures.push(marker.path);
|
|
157
169
|
}
|
|
158
170
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
171
|
+
if (failures.length > 0) {
|
|
172
|
+
return check("markers", "FAIL", `Managed block markers failed verification: ${failures.join(", ")}.`);
|
|
173
|
+
}
|
|
174
|
+
if (legacyPaths.length > 0) {
|
|
175
|
+
return check("markers", "DEGRADED", `Legacy managed routing blocks need migration: ${legacyPaths.join(", ")}.`);
|
|
176
|
+
}
|
|
177
|
+
return check("markers", "PASS", "All managed block markers are intact.");
|
|
162
178
|
}
|
|
163
179
|
async function checkProbe(id, probe) {
|
|
164
180
|
if (probe === undefined) {
|
|
@@ -177,21 +193,115 @@ async function checkProbe(id, probe) {
|
|
|
177
193
|
return check(id, "FAIL", "Probe failed.");
|
|
178
194
|
}
|
|
179
195
|
}
|
|
196
|
+
function checkLifecycleSummary(manifest, config) {
|
|
197
|
+
if (manifest === undefined || config === undefined) {
|
|
198
|
+
return check("lifecycle-summary", "UNKNOWN", "Lifecycle summary cannot be assessed without a valid manifest and configuration.");
|
|
199
|
+
}
|
|
200
|
+
const hasLifecycleSummary = config.profiles.length > 0 &&
|
|
201
|
+
resolveProfiles(config.profiles).capabilities.includes("lifecycle-summary");
|
|
202
|
+
if (!hasLifecycleSummary) {
|
|
203
|
+
return check("lifecycle-summary", "PASS", "Lifecycle summary has no harness-specific degradation.");
|
|
204
|
+
}
|
|
205
|
+
const registrations = manifest.harness.map((id) => ({
|
|
206
|
+
id,
|
|
207
|
+
registration: harnessDescriptor(id).control.registrations.find((candidate) => candidate.capability === "lifecycle-summary")
|
|
208
|
+
}));
|
|
209
|
+
const missing = registrations
|
|
210
|
+
.filter(({ registration }) => registration === undefined)
|
|
211
|
+
.map(({ id }) => id);
|
|
212
|
+
if (missing.length > 0) {
|
|
213
|
+
return check("lifecycle-summary", "UNKNOWN", `Lifecycle summary registration is missing for ${missing.join(", ")}.`);
|
|
214
|
+
}
|
|
215
|
+
const unsupported = registrations
|
|
216
|
+
.filter(({ registration }) => registration?.support === "unsupported")
|
|
217
|
+
.map(({ id }) => id);
|
|
218
|
+
if (unsupported.length > 0) {
|
|
219
|
+
return check("lifecycle-summary", "UNSUPPORTED", `Lifecycle summary is not dispatched for ${unsupported.join(", ")}; advisory runtime wiring is unavailable.`);
|
|
220
|
+
}
|
|
221
|
+
const degraded = registrations
|
|
222
|
+
.filter(({ registration }) => registration?.support === "degraded")
|
|
223
|
+
.map(({ id }) => id);
|
|
224
|
+
if (degraded.length > 0) {
|
|
225
|
+
return check("lifecycle-summary", "DEGRADED", `Lifecycle summary is degraded for ${degraded.join(", ")}.`);
|
|
226
|
+
}
|
|
227
|
+
const unknown = registrations
|
|
228
|
+
.filter(({ registration }) => registration?.support === "unknown")
|
|
229
|
+
.map(({ id }) => id);
|
|
230
|
+
if (unknown.length > 0) {
|
|
231
|
+
return check("lifecycle-summary", "UNKNOWN", `Lifecycle summary support is unknown for ${unknown.join(", ")}.`);
|
|
232
|
+
}
|
|
233
|
+
return check("lifecycle-summary", "PASS", "Lifecycle summary is reachable for every selected harness.");
|
|
234
|
+
}
|
|
235
|
+
async function checkSurfaceInventory(root, manifest, config) {
|
|
236
|
+
if (manifest === undefined || config === undefined) {
|
|
237
|
+
return {
|
|
238
|
+
check: check("surface-inventory", "UNKNOWN", "Harness surfaces cannot be inventoried without a valid manifest and configuration."),
|
|
239
|
+
surfaces: []
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
try {
|
|
243
|
+
const surfaces = await inspectHarnessSurfaces({
|
|
244
|
+
root,
|
|
245
|
+
scope: manifest.scope,
|
|
246
|
+
harness: manifest.harness,
|
|
247
|
+
profiles: config.profiles
|
|
248
|
+
});
|
|
249
|
+
const unknown = surfaces.filter(({ status }) => status === "unknown");
|
|
250
|
+
return {
|
|
251
|
+
check: check("surface-inventory", unknown.length > 0 ? "UNKNOWN" : "PASS", unknown.length > 0
|
|
252
|
+
? `${unknown.length} harness surface(s) could not be inspected.`
|
|
253
|
+
: "Harness surfaces were inventoried without exposing settings values."),
|
|
254
|
+
surfaces
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
return {
|
|
259
|
+
check: check("surface-inventory", "UNKNOWN", "Harness surfaces could not be inspected safely."),
|
|
260
|
+
surfaces: []
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
async function checkRegistrationDrift(root, manifest, config) {
|
|
265
|
+
if (manifest === undefined || config === undefined) {
|
|
266
|
+
return check("registration-drift", "UNKNOWN", "Hook registration drift cannot be assessed without a valid manifest and configuration.");
|
|
267
|
+
}
|
|
268
|
+
try {
|
|
269
|
+
const statuses = await inspectHarnessRegistrations({
|
|
270
|
+
root,
|
|
271
|
+
manifest,
|
|
272
|
+
config
|
|
273
|
+
});
|
|
274
|
+
const drifted = statuses
|
|
275
|
+
.filter(({ registered }) => !registered)
|
|
276
|
+
.map(({ harness }) => harness);
|
|
277
|
+
return drifted.length === 0
|
|
278
|
+
? check("registration-drift", "PASS", "Managed hook registrations match the desired capabilities.")
|
|
279
|
+
: check("registration-drift", "FAIL", `Hook registration drift detected for ${drifted.join(", ")}; run agent-ops update.`, "UPDATE_REQUIRED");
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
return check("registration-drift", "UNKNOWN", "Hook registration drift could not be assessed safely.");
|
|
283
|
+
}
|
|
284
|
+
}
|
|
180
285
|
export async function doctorInstallation(options) {
|
|
181
286
|
const manifest = await checkManifest(options.root);
|
|
182
287
|
const config = await checkConfig(options.root);
|
|
288
|
+
const surfaceInventory = await checkSurfaceInventory(options.root, manifest.manifest, config.config);
|
|
183
289
|
const checks = [
|
|
184
290
|
checkNodeVersion(options.nodeVersion ?? process.versions.node),
|
|
185
291
|
manifest.check,
|
|
186
292
|
config.check,
|
|
187
293
|
await checkArtifacts(options.root, manifest.manifest),
|
|
188
294
|
await checkMarkers(options.root, manifest.manifest),
|
|
295
|
+
surfaceInventory.check,
|
|
296
|
+
await checkRegistrationDrift(options.root, manifest.manifest, config.config),
|
|
189
297
|
await checkProbe("hook-registration", options.probes?.hookRegistration),
|
|
298
|
+
checkLifecycleSummary(manifest.manifest, config.config),
|
|
190
299
|
await checkProbe("repository-trust", options.probes?.repositoryTrust),
|
|
191
300
|
await checkProbe("smoke-availability", options.probes?.smokeAvailability)
|
|
192
301
|
];
|
|
193
302
|
return {
|
|
194
303
|
checks,
|
|
304
|
+
surfaces: surfaceInventory.surfaces,
|
|
195
305
|
...(manifest.manifest === undefined
|
|
196
306
|
? {}
|
|
197
307
|
: { manifest: manifest.manifest }),
|