@gmickel/gno 1.37.1 → 1.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/skill/README.md +2 -0
- package/browser-extension/artifacts/{gno-browser-clipper-v1.37.1.zip → gno-browser-clipper-v1.39.0.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.39.0.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +1 -1
- package/spec/cli.md +243 -26
- package/spec/output-schemas/agents-mutation.schema.json +108 -0
- package/spec/output-schemas/agents-verify.schema.json +89 -0
- package/src/cli/commands/agents/block.ts +164 -0
- package/src/cli/commands/agents/commands.ts +413 -0
- package/src/cli/commands/agents/engine.ts +417 -0
- package/src/cli/commands/agents/harnesses.ts +298 -0
- package/src/cli/commands/agents/index.ts +35 -0
- package/src/cli/commands/cleanup.ts +8 -2
- package/src/cli/commands/collection/clear-embeddings.ts +6 -1
- package/src/cli/commands/completion/scripts.ts +5 -0
- package/src/cli/commands/doctor-activation.ts +5 -1
- package/src/cli/commands/doctor.ts +72 -2
- package/src/cli/commands/embed.ts +227 -194
- package/src/cli/commands/index-cmd.ts +74 -50
- package/src/cli/commands/init.ts +5 -1
- package/src/cli/commands/profile-apply.ts +5 -1
- package/src/cli/commands/setup-activation.ts +2 -1
- package/src/cli/commands/setup.ts +2 -1
- package/src/cli/commands/shared.ts +5 -1
- package/src/cli/commands/status.ts +5 -1
- package/src/cli/commands/tags.ts +18 -3
- package/src/cli/commands/update.ts +34 -27
- package/src/cli/commands/vec.ts +13 -4
- package/src/cli/errors.ts +3 -2
- package/src/cli/program.ts +449 -194
- package/src/config/defaults.ts +2 -0
- package/src/config/index.ts +3 -0
- package/src/config/types.ts +32 -1
- package/src/core/file-lock.ts +16 -4
- package/src/core/write-lease.ts +354 -0
- package/src/embed/backlog.ts +9 -1
- package/src/embed/retry.ts +116 -3
- package/src/sdk/client.ts +3 -1
- package/src/sdk/embed.ts +8 -3
- package/src/sdk/types.ts +2 -0
- package/src/serve/embed-scheduler.ts +8 -0
- package/src/serve/resident-runtime.ts +5 -1
- package/src/store/sqlite/adapter.ts +28 -4
- package/src/store/sqlite/scoped-index.ts +5 -1
- package/src/store/vector/sqlite-vec.ts +2 -1
- package/browser-extension/artifacts/gno-browser-clipper-v1.37.1.zip.sha256 +0 -1
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GNO agents protocol block: content, markers, hashing, extraction.
|
|
3
|
+
*
|
|
4
|
+
* One compact, versioned instruction block bounded by stable BEGIN/END
|
|
5
|
+
* markers. Install/update/uninstall touch ONLY the owned block; content
|
|
6
|
+
* outside the markers stays byte-identical. The block content is static —
|
|
7
|
+
* identical on every machine — so a block is current exactly when its stamp
|
|
8
|
+
* version matches the installed release and its hash matches its body.
|
|
9
|
+
*
|
|
10
|
+
* @module src/cli/commands/agents/block
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { CliError } from "../../errors.js";
|
|
14
|
+
|
|
15
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
16
|
+
// Constants
|
|
17
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
/** Version of the protocol block content. Bump on any content change. */
|
|
20
|
+
export const BLOCK_VERSION = 2;
|
|
21
|
+
|
|
22
|
+
/** Stable across block versions — never change these once shipped. */
|
|
23
|
+
export const BEGIN_MARKER = "<!-- gno:agents:begin -->";
|
|
24
|
+
export const END_MARKER = "<!-- gno:agents:end -->";
|
|
25
|
+
|
|
26
|
+
const STAMP_RE = /^<!-- gno-agents block v(\d+) sha256:([0-9a-f]{16}) /;
|
|
27
|
+
const HASH_PREFIX_LENGTH = 16;
|
|
28
|
+
|
|
29
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
30
|
+
// Rendering
|
|
31
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The protocol block body (between stamp line and end marker). Compact by
|
|
35
|
+
* design: the retrieval ladder + the writing contract. Detailed workflows live
|
|
36
|
+
* in the GNO skill; the block just points at it. No filesystem paths — the
|
|
37
|
+
* text is the same on every machine.
|
|
38
|
+
*/
|
|
39
|
+
export function renderBlockBody(): string {
|
|
40
|
+
return `## GNO knowledge retrieval
|
|
41
|
+
|
|
42
|
+
Local knowledge search over indexed collections. Source files are the truth; the GNO index is disposable, machine-local.
|
|
43
|
+
|
|
44
|
+
Ladder — scope to a collection first (\`--collection <name>\`):
|
|
45
|
+
|
|
46
|
+
1. Exact term/identifier/quote/error: \`gno search "<text>"\`
|
|
47
|
+
2. Entity or known document: \`gno query "<question>" --fast -n 10\`
|
|
48
|
+
3. Multi-document evidence for a goal: \`gno context build "<goal>" --budget 12000\`
|
|
49
|
+
4. Change/dependency questions: \`gno changes\` / \`gno diff <doc>\` / \`gno impact <doc>\`
|
|
50
|
+
5. Generated factual answer: \`gno ask "<question>" --verify\` (abstention is valid)
|
|
51
|
+
6. Expected document missing: reformulate + re-check collection scope (\`gno query diagnose "<query>" --target <doc>\`) before any grep fallback.
|
|
52
|
+
|
|
53
|
+
Writing: retrieve first — a question alone is read-only. Edit an existing canonical note in its source file; \`gno capture\` creates genuinely new notes (collection, title/path, source kind, provenance) — never an update API. After writes: reindex the collection, verify retrieval.
|
|
54
|
+
|
|
55
|
+
Cite with gno:// URIs. Advanced retrieval (structured queries, filters, backlinks, similar, capture recipes) lives in the \`gno\` skill: load it (\`/gno\`) when installed, otherwise run \`gno skill install --scope user\` first.`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** SHA-256 hex digest of the body, truncated for the stamp line. */
|
|
59
|
+
export function hashBlockBody(body: string): string {
|
|
60
|
+
const hasher = new Bun.CryptoHasher("sha256");
|
|
61
|
+
hasher.update(body);
|
|
62
|
+
return hasher.digest("hex").slice(0, HASH_PREFIX_LENGTH);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Render the stamp line carrying version + body hash. */
|
|
66
|
+
export function renderStampLine(body: string): string {
|
|
67
|
+
return `<!-- gno-agents block v${BLOCK_VERSION} sha256:${hashBlockBody(body)} — managed by \`gno agents\`; manual edits inside the markers are overwritten -->`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Render the complete block: BEGIN marker, stamp, body, END marker. */
|
|
71
|
+
export function renderBlock(): string {
|
|
72
|
+
const body = renderBlockBody();
|
|
73
|
+
return `${BEGIN_MARKER}\n${renderStampLine(body)}\n${body}\n${END_MARKER}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** True when the extracted stamp's hash matches the extracted body. */
|
|
77
|
+
export function stampAuthenticates(block: ExtractedBlock): boolean {
|
|
78
|
+
return block.stamp !== null && block.stamp.hash === hashBlockBody(block.body);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
82
|
+
// Extraction & Validation
|
|
83
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
export interface ExtractedBlock {
|
|
86
|
+
/** Offset of the BEGIN marker in the file content. */
|
|
87
|
+
start: number;
|
|
88
|
+
/** Offset just past the END marker. */
|
|
89
|
+
end: number;
|
|
90
|
+
/** Everything between the markers (stamp line + body), without markers. */
|
|
91
|
+
inner: string;
|
|
92
|
+
/** Body without the stamp line (equal to inner when no stamp present). */
|
|
93
|
+
body: string;
|
|
94
|
+
/** Parsed stamp, when present and well-formed. */
|
|
95
|
+
stamp: { version: number; hash: string } | null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export type BlockExtraction =
|
|
99
|
+
| { found: false }
|
|
100
|
+
| { found: true; block: ExtractedBlock };
|
|
101
|
+
|
|
102
|
+
function countOccurrences(haystack: string, needle: string): number {
|
|
103
|
+
let count = 0;
|
|
104
|
+
let idx = haystack.indexOf(needle);
|
|
105
|
+
while (idx !== -1) {
|
|
106
|
+
count += 1;
|
|
107
|
+
idx = haystack.indexOf(needle, idx + needle.length);
|
|
108
|
+
}
|
|
109
|
+
return count;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const MARKER_GUIDANCE =
|
|
113
|
+
"Fix or remove the markers manually (or restore the file from its .gno-agents.bak backup), then re-run.";
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Extract the managed block from file content.
|
|
117
|
+
* Fail-closed: malformed or duplicate markers throw with guidance — the
|
|
118
|
+
* installer never guesses or "repairs".
|
|
119
|
+
*/
|
|
120
|
+
export function extractBlock(
|
|
121
|
+
content: string,
|
|
122
|
+
filePath: string
|
|
123
|
+
): BlockExtraction {
|
|
124
|
+
const begins = countOccurrences(content, BEGIN_MARKER);
|
|
125
|
+
const ends = countOccurrences(content, END_MARKER);
|
|
126
|
+
|
|
127
|
+
if (begins === 0 && ends === 0) {
|
|
128
|
+
return { found: false };
|
|
129
|
+
}
|
|
130
|
+
if (begins !== 1 || ends !== 1) {
|
|
131
|
+
throw new CliError(
|
|
132
|
+
"VALIDATION",
|
|
133
|
+
`Malformed GNO agents markers in ${filePath}: found ${begins} BEGIN and ${ends} END marker(s), expected exactly one of each. ${MARKER_GUIDANCE}`
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const start = content.indexOf(BEGIN_MARKER);
|
|
138
|
+
const endMarkerStart = content.indexOf(END_MARKER);
|
|
139
|
+
if (endMarkerStart < start) {
|
|
140
|
+
throw new CliError(
|
|
141
|
+
"VALIDATION",
|
|
142
|
+
`Malformed GNO agents markers in ${filePath}: END marker appears before BEGIN marker. ${MARKER_GUIDANCE}`
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const end = endMarkerStart + END_MARKER.length;
|
|
147
|
+
const rawInner = content.slice(start + BEGIN_MARKER.length, endMarkerStart);
|
|
148
|
+
// Trim exactly the structural newlines install added around the inner text.
|
|
149
|
+
const inner = rawInner.replace(/^\n/, "").replace(/\n$/, "");
|
|
150
|
+
|
|
151
|
+
const newlineIdx = inner.indexOf("\n");
|
|
152
|
+
const firstLine = newlineIdx === -1 ? inner : inner.slice(0, newlineIdx);
|
|
153
|
+
const stampMatch = STAMP_RE.exec(firstLine);
|
|
154
|
+
const version = stampMatch ? Number(stampMatch[1]) : Number.NaN;
|
|
155
|
+
// An absurd version (not a safe integer) is an unparseable stamp, not a
|
|
156
|
+
// numeric verdict — it would otherwise serialize as null in JSON receipts.
|
|
157
|
+
const stamp =
|
|
158
|
+
stampMatch && Number.isSafeInteger(version)
|
|
159
|
+
? { version, hash: stampMatch[2] ?? "" }
|
|
160
|
+
: null;
|
|
161
|
+
const body = stamp && newlineIdx !== -1 ? inner.slice(newlineIdx + 1) : inner;
|
|
162
|
+
|
|
163
|
+
return { found: true, block: { start, end, inner, body, stamp } };
|
|
164
|
+
}
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `gno agents` command runners: install, update, verify, uninstall.
|
|
3
|
+
*
|
|
4
|
+
* @module src/cli/commands/agents/commands
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { CliError } from "../../errors.js";
|
|
8
|
+
import { getGlobals } from "../../program.js";
|
|
9
|
+
import {
|
|
10
|
+
BLOCK_VERSION,
|
|
11
|
+
extractBlock,
|
|
12
|
+
renderBlock,
|
|
13
|
+
stampAuthenticates,
|
|
14
|
+
} from "./block.js";
|
|
15
|
+
import {
|
|
16
|
+
applyPlan,
|
|
17
|
+
decodeInstructionFile,
|
|
18
|
+
type PlanMode,
|
|
19
|
+
planTargets,
|
|
20
|
+
planWrites,
|
|
21
|
+
type TargetPlan,
|
|
22
|
+
unifiedDiff,
|
|
23
|
+
} from "./engine.js";
|
|
24
|
+
import {
|
|
25
|
+
type HarnessId,
|
|
26
|
+
HARNESS_IDS,
|
|
27
|
+
type ResolvedTarget,
|
|
28
|
+
resolveTargets,
|
|
29
|
+
} from "./harnesses.js";
|
|
30
|
+
|
|
31
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
32
|
+
// Shared
|
|
33
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
export interface AgentsOptions {
|
|
36
|
+
target?: HarnessId | "all";
|
|
37
|
+
extraDirs?: string[];
|
|
38
|
+
dryRun?: boolean;
|
|
39
|
+
json?: boolean;
|
|
40
|
+
quiet?: boolean;
|
|
41
|
+
/** Override for testing / sandboxed live verification. */
|
|
42
|
+
homeDir?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function safeGetGlobals(): { json: boolean; quiet: boolean } {
|
|
46
|
+
try {
|
|
47
|
+
return getGlobals();
|
|
48
|
+
} catch {
|
|
49
|
+
return { json: false, quiet: false };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function parseTargetOption(raw: string): HarnessId | "all" {
|
|
54
|
+
if (raw === "all" || (HARNESS_IDS as string[]).includes(raw)) {
|
|
55
|
+
return raw as HarnessId | "all";
|
|
56
|
+
}
|
|
57
|
+
throw new CliError(
|
|
58
|
+
"VALIDATION",
|
|
59
|
+
`Invalid target: ${raw}. Must be one of ${HARNESS_IDS.join(", ")}, or 'all'.`
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function outputSettings(opts: AgentsOptions): {
|
|
64
|
+
json: boolean;
|
|
65
|
+
quiet: boolean;
|
|
66
|
+
} {
|
|
67
|
+
const globals = safeGetGlobals();
|
|
68
|
+
return {
|
|
69
|
+
json: opts.json ?? globals.json,
|
|
70
|
+
quiet: opts.quiet ?? globals.quiet,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface TargetReport {
|
|
75
|
+
target: string;
|
|
76
|
+
label: string;
|
|
77
|
+
path: string;
|
|
78
|
+
action: string;
|
|
79
|
+
detected: boolean;
|
|
80
|
+
via?: string;
|
|
81
|
+
detail?: string;
|
|
82
|
+
backup?: string | null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function reportFor(
|
|
86
|
+
plan: TargetPlan,
|
|
87
|
+
backup: string | null | undefined
|
|
88
|
+
): TargetReport {
|
|
89
|
+
return {
|
|
90
|
+
target: plan.target.id,
|
|
91
|
+
label: plan.target.label,
|
|
92
|
+
path: plan.target.file,
|
|
93
|
+
action: plan.action,
|
|
94
|
+
detected: plan.target.detected,
|
|
95
|
+
...(plan.via && { via: plan.via }),
|
|
96
|
+
...(plan.detail && { detail: plan.detail }),
|
|
97
|
+
...(backup !== undefined && { backup }),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function printHumanReports(reports: TargetReport[]): void {
|
|
102
|
+
for (const r of reports) {
|
|
103
|
+
const via = r.via ? ` (${r.detail ?? `via ${r.via}`})` : "";
|
|
104
|
+
const detail = !r.via && r.detail ? `: ${r.detail}` : "";
|
|
105
|
+
process.stdout.write(
|
|
106
|
+
`${r.action.padEnd(12)} ${r.target.padEnd(10)} ${r.path}${via}${detail}\n`
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
112
|
+
// Install / Update / Uninstall (shared mutation runner)
|
|
113
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
async function runMutation(
|
|
116
|
+
verb: "install" | "update" | "uninstall",
|
|
117
|
+
mode: PlanMode,
|
|
118
|
+
opts: AgentsOptions
|
|
119
|
+
): Promise<void> {
|
|
120
|
+
const { json, quiet } = outputSettings(opts);
|
|
121
|
+
const dryRun = opts.dryRun ?? false;
|
|
122
|
+
|
|
123
|
+
const targets = resolveTargets(opts.target ?? "all", {
|
|
124
|
+
homeDir: opts.homeDir,
|
|
125
|
+
extraDirs: opts.extraDirs,
|
|
126
|
+
});
|
|
127
|
+
const plans = await planTargets(targets, mode);
|
|
128
|
+
|
|
129
|
+
const reports: TargetReport[] = [];
|
|
130
|
+
const diffs: string[] = [];
|
|
131
|
+
const failedPaths: string[] = [];
|
|
132
|
+
let validationErrors = 0;
|
|
133
|
+
|
|
134
|
+
for (const plan of plans) {
|
|
135
|
+
if (plan.action === "error") {
|
|
136
|
+
failedPaths.push(plan.target.file);
|
|
137
|
+
if (plan.errorCode !== "RUNTIME") {
|
|
138
|
+
validationErrors += 1;
|
|
139
|
+
}
|
|
140
|
+
reports.push(reportFor(plan, undefined));
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (!planWrites(plan)) {
|
|
144
|
+
reports.push(reportFor(plan, undefined));
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (dryRun) {
|
|
148
|
+
diffs.push(
|
|
149
|
+
unifiedDiff(plan.oldContent, plan.newContent, plan.target.file)
|
|
150
|
+
);
|
|
151
|
+
reports.push(reportFor(plan, null));
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
// A failing write must not abort the run before the receipt is emitted:
|
|
155
|
+
// earlier targets may already have been written, and the operator needs
|
|
156
|
+
// to see exactly which. Record an `error` row and keep going.
|
|
157
|
+
try {
|
|
158
|
+
reports.push(reportFor(plan, await applyPlan(plan)));
|
|
159
|
+
} catch (err) {
|
|
160
|
+
failedPaths.push(plan.target.file);
|
|
161
|
+
reports.push({
|
|
162
|
+
...reportFor(plan, undefined),
|
|
163
|
+
action: "error",
|
|
164
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Fallback: when the installer could not apply the block somewhere, hand
|
|
170
|
+
// the operator the exact block to paste (install/update only — uninstall
|
|
171
|
+
// guidance is in the per-target detail).
|
|
172
|
+
const manualBlock =
|
|
173
|
+
failedPaths.length > 0 && mode === "install" ? renderBlock() : undefined;
|
|
174
|
+
|
|
175
|
+
if (json) {
|
|
176
|
+
process.stdout.write(
|
|
177
|
+
`${JSON.stringify(
|
|
178
|
+
{
|
|
179
|
+
command: verb,
|
|
180
|
+
blockVersion: BLOCK_VERSION,
|
|
181
|
+
dryRun,
|
|
182
|
+
results: reports,
|
|
183
|
+
...(dryRun && { diffs }),
|
|
184
|
+
...(manualBlock !== undefined && { manualBlock }),
|
|
185
|
+
},
|
|
186
|
+
null,
|
|
187
|
+
2
|
|
188
|
+
)}\n`
|
|
189
|
+
);
|
|
190
|
+
} else if (!quiet) {
|
|
191
|
+
printHumanReports(reports);
|
|
192
|
+
if (dryRun) {
|
|
193
|
+
for (const diff of diffs) {
|
|
194
|
+
if (diff) {
|
|
195
|
+
process.stdout.write(`\n${diff}\n`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
process.stdout.write("\nDry run — nothing was written.\n");
|
|
199
|
+
}
|
|
200
|
+
if (manualBlock !== undefined) {
|
|
201
|
+
process.stdout.write(
|
|
202
|
+
`\nCould not apply the block to: ${failedPaths.join(", ")}\n` +
|
|
203
|
+
"Append this block to the file yourself (replacing any existing gno:agents block):\n\n" +
|
|
204
|
+
`${manualBlock}\n`
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (failedPaths.length > 0) {
|
|
210
|
+
// Exit-code contract (spec/cli.md): 1 = validation (malformed markers,
|
|
211
|
+
// non-UTF-8), 2 = runtime (I/O). Any validation failure makes it 1.
|
|
212
|
+
throw new CliError(
|
|
213
|
+
validationErrors > 0 ? "VALIDATION" : "RUNTIME",
|
|
214
|
+
`${verb} failed for ${failedPaths.length} target(s); see per-target detail above. Nothing was written to the failing file(s).`
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Install or refresh the protocol block. Both verbs converge the block to
|
|
221
|
+
* the current release: install appends when absent, update replaces an
|
|
222
|
+
* older/stale block in place; a current block is a no-op for either.
|
|
223
|
+
*/
|
|
224
|
+
export function installAgents(
|
|
225
|
+
opts: AgentsOptions = {},
|
|
226
|
+
verb: "install" | "update" = "install"
|
|
227
|
+
): Promise<void> {
|
|
228
|
+
return runMutation(verb, "install", opts);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Remove the protocol block and its markers, leaving the rest untouched. */
|
|
232
|
+
export function uninstallAgents(opts: AgentsOptions = {}): Promise<void> {
|
|
233
|
+
return runMutation("uninstall", "uninstall", opts);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
237
|
+
// Verify
|
|
238
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
239
|
+
|
|
240
|
+
type VerifyStatus =
|
|
241
|
+
| "ok"
|
|
242
|
+
| "outdated"
|
|
243
|
+
| "missing"
|
|
244
|
+
| "malformed"
|
|
245
|
+
| "error"
|
|
246
|
+
| "covered"
|
|
247
|
+
| "not-detected";
|
|
248
|
+
|
|
249
|
+
interface VerifyReport {
|
|
250
|
+
target: string;
|
|
251
|
+
label: string;
|
|
252
|
+
path: string;
|
|
253
|
+
status: VerifyStatus;
|
|
254
|
+
detected: boolean;
|
|
255
|
+
via?: string;
|
|
256
|
+
detail?: string;
|
|
257
|
+
blockVersion?: number;
|
|
258
|
+
hashOk?: boolean;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** Inner text (stamp + body, no markers) of the current release's block. */
|
|
262
|
+
function expectedInner(): string {
|
|
263
|
+
return renderBlock().split("\n").slice(1, -1).join("\n");
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async function verifyTarget(
|
|
267
|
+
target: ResolvedTarget,
|
|
268
|
+
runContext: {
|
|
269
|
+
resolvedIds: Set<string>;
|
|
270
|
+
requiredCovering: Set<string>;
|
|
271
|
+
/** Real-file identity → id of the target that owns verification. */
|
|
272
|
+
owners: Map<string, string>;
|
|
273
|
+
}
|
|
274
|
+
): Promise<VerifyReport> {
|
|
275
|
+
const base = {
|
|
276
|
+
target: target.id,
|
|
277
|
+
label: target.label,
|
|
278
|
+
path: target.file,
|
|
279
|
+
detected: target.detected,
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
if (!(target.detected || runContext.requiredCovering.has(target.id))) {
|
|
283
|
+
return { ...base, status: "not-detected" };
|
|
284
|
+
}
|
|
285
|
+
if (target.coveredBy) {
|
|
286
|
+
if (!runContext.resolvedIds.has(target.coveredBy)) {
|
|
287
|
+
return {
|
|
288
|
+
...base,
|
|
289
|
+
status: "missing",
|
|
290
|
+
detail: `covered via ${target.coveredBy}, but ${target.coveredBy} was not resolved in this run`,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
return {
|
|
294
|
+
...base,
|
|
295
|
+
status: "covered",
|
|
296
|
+
via: target.coveredBy,
|
|
297
|
+
detail: `covered via ${target.coveredBy}`,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
const owner = runContext.owners.get(target.realFile);
|
|
301
|
+
if (owner) {
|
|
302
|
+
return {
|
|
303
|
+
...base,
|
|
304
|
+
status: "covered",
|
|
305
|
+
via: owner,
|
|
306
|
+
detail: `covered via ${owner} (same file)`,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
runContext.owners.set(target.realFile, target.id);
|
|
310
|
+
|
|
311
|
+
const file = Bun.file(target.file);
|
|
312
|
+
if (!(await file.exists())) {
|
|
313
|
+
return { ...base, status: "missing", detail: "instruction file not found" };
|
|
314
|
+
}
|
|
315
|
+
let bytes: Uint8Array;
|
|
316
|
+
try {
|
|
317
|
+
bytes = await file.bytes();
|
|
318
|
+
} catch (err) {
|
|
319
|
+
return {
|
|
320
|
+
...base,
|
|
321
|
+
status: "error",
|
|
322
|
+
detail: `could not read instruction file: ${err instanceof Error ? err.message : String(err)}`,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
let extraction: ReturnType<typeof extractBlock>;
|
|
326
|
+
try {
|
|
327
|
+
const { content } = decodeInstructionFile(bytes, target.file);
|
|
328
|
+
extraction = extractBlock(content, target.file);
|
|
329
|
+
} catch (err) {
|
|
330
|
+
return {
|
|
331
|
+
...base,
|
|
332
|
+
status: "malformed",
|
|
333
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
if (!extraction.found) {
|
|
337
|
+
return { ...base, status: "missing", detail: "no GNO agents block" };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const { block } = extraction;
|
|
341
|
+
const hashOk = stampAuthenticates(block);
|
|
342
|
+
const versionOk = block.stamp?.version === BLOCK_VERSION;
|
|
343
|
+
const isCurrent = versionOk && hashOk && block.inner === expectedInner();
|
|
344
|
+
const versioned = { blockVersion: block.stamp?.version, hashOk };
|
|
345
|
+
if (isCurrent) {
|
|
346
|
+
return { ...base, status: "ok", ...versioned };
|
|
347
|
+
}
|
|
348
|
+
const detail = !block.stamp
|
|
349
|
+
? "block has no valid stamp line (missing or unparseable) — run `gno agents update`"
|
|
350
|
+
: !hashOk
|
|
351
|
+
? "block content does not match its stamp hash (edited inside markers?) — run `gno agents update`"
|
|
352
|
+
: !versionOk
|
|
353
|
+
? `block v${block.stamp?.version ?? "?"} does not match installed release v${BLOCK_VERSION} — run \`gno agents update\``
|
|
354
|
+
: "block content differs from the installed release — run `gno agents update`";
|
|
355
|
+
return { ...base, status: "outdated", ...versioned, detail };
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
export async function verifyAgents(opts: AgentsOptions = {}): Promise<void> {
|
|
359
|
+
const { json, quiet } = outputSettings(opts);
|
|
360
|
+
const targets = resolveTargets(opts.target ?? "all", {
|
|
361
|
+
homeDir: opts.homeDir,
|
|
362
|
+
extraDirs: opts.extraDirs,
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
const resolvedIds = new Set(targets.map((t) => t.id as string));
|
|
366
|
+
const requiredCovering = new Set<string>();
|
|
367
|
+
for (const t of targets) {
|
|
368
|
+
if (t.detected && t.coveredBy) {
|
|
369
|
+
requiredCovering.add(t.coveredBy);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
const owners = new Map<string, string>();
|
|
373
|
+
const results: VerifyReport[] = [];
|
|
374
|
+
for (const target of targets) {
|
|
375
|
+
results.push(
|
|
376
|
+
await verifyTarget(target, { resolvedIds, requiredCovering, owners })
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const failing = results.filter(
|
|
381
|
+
(r) => !["ok", "covered", "not-detected"].includes(r.status)
|
|
382
|
+
);
|
|
383
|
+
const ok = failing.length === 0;
|
|
384
|
+
|
|
385
|
+
if (json) {
|
|
386
|
+
process.stdout.write(
|
|
387
|
+
`${JSON.stringify(
|
|
388
|
+
{ command: "verify", blockVersion: BLOCK_VERSION, ok, results },
|
|
389
|
+
null,
|
|
390
|
+
2
|
|
391
|
+
)}\n`
|
|
392
|
+
);
|
|
393
|
+
} else if (!quiet) {
|
|
394
|
+
for (const r of results) {
|
|
395
|
+
const detail = r.detail ? ` — ${r.detail}` : "";
|
|
396
|
+
process.stdout.write(
|
|
397
|
+
`${r.status.padEnd(13)} ${r.target.padEnd(10)} ${r.path}${detail}\n`
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (!ok) {
|
|
403
|
+
// 2 when the only failures were unreadable files (could not check),
|
|
404
|
+
// 1 for any content verdict (outdated / missing / malformed).
|
|
405
|
+
const allRuntime = failing.every((r) => r.status === "error");
|
|
406
|
+
throw new CliError(
|
|
407
|
+
allRuntime ? "RUNTIME" : "VALIDATION",
|
|
408
|
+
`verification failed for ${failing.length} target(s): ${failing
|
|
409
|
+
.map((r) => `${r.target} (${r.status})`)
|
|
410
|
+
.join(", ")}`
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
}
|