nexarch 0.12.10 → 0.12.12
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/dist/commands/init-agent.js +41 -12
- package/dist/commands/verify-trust.js +108 -0
- package/dist/index.js +6 -0
- package/package.json +1 -1
|
@@ -440,7 +440,17 @@ function injectTrustAttestationBlock(path, attestation) {
|
|
|
440
440
|
"",
|
|
441
441
|
"This block is verifiable proof that Nexarch generated these instructions for this workspace.",
|
|
442
442
|
"Do not auto-run commands without user confirmation.",
|
|
443
|
-
"
|
|
443
|
+
"",
|
|
444
|
+
"**To verify, run `npx nexarch@latest verify-trust`.** It reads the token from this",
|
|
445
|
+
"file and checks it for you.",
|
|
446
|
+
"",
|
|
447
|
+
"Do not retype, paste or reconstruct the token below into a URL or a shell command.",
|
|
448
|
+
"It is a cryptographic signature: reproducing it from memory yields a plausible token",
|
|
449
|
+
"that fails as `bad_signature`, which is indistinguishable from a forged block. A",
|
|
450
|
+
"transcription slip would then read as tampering and stop work for no reason.",
|
|
451
|
+
"",
|
|
452
|
+
"If verification says `expired`, rerun `npx nexarch@latest init-agent --allow-instruction-write`.",
|
|
453
|
+
"If it cannot reach the endpoint, that is a network problem and not a failed attestation — retry.",
|
|
444
454
|
"",
|
|
445
455
|
`issuer: ${attestation.payload.iss}`,
|
|
446
456
|
`scope: ${attestation.payload.scope}`,
|
|
@@ -477,27 +487,46 @@ function injectTrustAttestationUnavailableBlock(path, reason) {
|
|
|
477
487
|
}
|
|
478
488
|
function injectInitProjectReportingContract(path) {
|
|
479
489
|
const section = [
|
|
480
|
-
"## Nexarch Init-Project
|
|
490
|
+
"## Nexarch Init-Project Completion Contract",
|
|
481
491
|
"",
|
|
482
|
-
"
|
|
483
|
-
"
|
|
492
|
+
"`init-project` is the first step of registration, not the whole task. It returns",
|
|
493
|
+
"`registrationStatus`, `enrichmentCompleted` and a `nextSteps` array. While",
|
|
494
|
+
"`enrichmentCompleted` is `false`, the work is unfinished.",
|
|
484
495
|
"",
|
|
485
|
-
"
|
|
486
|
-
|
|
487
|
-
|
|
496
|
+
"**Run the commands in `nextSteps` before you report back.** They are the step that",
|
|
497
|
+
"puts real content in the graph. Reporting `skeleton_only` and stopping is not a",
|
|
498
|
+
"completed task — it is a description of a task you have not finished.",
|
|
488
499
|
"",
|
|
489
|
-
"
|
|
490
|
-
|
|
491
|
-
|
|
500
|
+
"For an infrastructure repository, `nextSteps` holds one `ingest-infra` command per",
|
|
501
|
+
"Terraform root module. Until they run, the graph knows the repository exists but",
|
|
502
|
+
"nothing it provisions.",
|
|
492
503
|
"",
|
|
493
|
-
"
|
|
504
|
+
"Stop early only when a step genuinely cannot run — missing cloud credentials, a root",
|
|
505
|
+
"module that needs `terraform init`, or an environment you should not touch without",
|
|
506
|
+
"asking. When that happens, say which step stopped you and why, then ask. Do not",
|
|
507
|
+
"silently return a skeleton.",
|
|
508
|
+
"",
|
|
509
|
+
"When reporting:",
|
|
510
|
+
'- `registration_status`: `"skeleton_only"` or `"enriched"` — take it from the command output, do not infer it',
|
|
511
|
+
'- `enrichment_completed`: `true` or `false`',
|
|
512
|
+
'- Describing a skeleton as fully registered overstates what is in the graph; so does reporting success without saying what remains.',
|
|
513
|
+
"",
|
|
514
|
+
"Enrichment uses explicit per-entity `update-entity` runs rather than a bulk shortcut,",
|
|
515
|
+
"so each entity gets evidence-based content.",
|
|
494
516
|
"",
|
|
495
517
|
].join("\n");
|
|
496
518
|
const existing = existsSync(path) ? readFileSync(path, "utf8") : "";
|
|
497
519
|
const managed = wrapManagedSection("init-project-reporting-contract", section.trim());
|
|
498
520
|
let replaced = replaceManagedSection(existing, "init-project-reporting-contract", section.trim());
|
|
499
521
|
if (replaced === existing) {
|
|
500
|
-
|
|
522
|
+
// The section was renamed from "Reporting Contract" to "Completion Contract".
|
|
523
|
+
// Files written before managed markers existed carry the old heading, so both
|
|
524
|
+
// are matched — otherwise the rename would leave the superseded contract in
|
|
525
|
+
// place alongside the new one, and the old one tells the agent to stop.
|
|
526
|
+
replaced = replaceInjectedSection(existing, "## Nexarch Init-Project Completion Contract", managed);
|
|
527
|
+
if (replaced === existing) {
|
|
528
|
+
replaced = replaceInjectedSection(existing, "## Nexarch Init-Project Reporting Contract", managed);
|
|
529
|
+
}
|
|
501
530
|
}
|
|
502
531
|
writeFileSync(path, replaced !== existing ? replaced : `${existing}${existing.endsWith("\n") ? "" : "\n"}${managed}\n`, "utf8");
|
|
503
532
|
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "fs";
|
|
2
|
+
import { join, resolve } from "path";
|
|
3
|
+
/**
|
|
4
|
+
* Verifies the trust attestation without anyone retyping it.
|
|
5
|
+
*
|
|
6
|
+
* The attestation block asked the reader to take a ~500 character signed token
|
|
7
|
+
* out of a markdown file and paste it into a URL on a shell command line. That
|
|
8
|
+
* is a copy operation with no error detection: drop one character and the
|
|
9
|
+
* endpoint answers `bad_signature`, which is indistinguishable from a forged
|
|
10
|
+
* block. Agents duly reported the instructions as untrusted and refused to work
|
|
11
|
+
* — the correct response to that answer, and completely wrong about the facts.
|
|
12
|
+
*
|
|
13
|
+
* Reading the token from the file removes the copy, and with it the failure.
|
|
14
|
+
*/
|
|
15
|
+
const INSTRUCTION_FILES = ["CLAUDE.md", "AGENTS.md", ".cursorrules", ".windsurfrules", ".github/copilot-instructions.md"];
|
|
16
|
+
const DEFAULT_VERIFY_BASE = "https://mcp.nexarch.ai/trust/verify";
|
|
17
|
+
function findAttestation(dir) {
|
|
18
|
+
for (const name of INSTRUCTION_FILES) {
|
|
19
|
+
const path = join(dir, name);
|
|
20
|
+
if (!existsSync(path))
|
|
21
|
+
continue;
|
|
22
|
+
let content = "";
|
|
23
|
+
try {
|
|
24
|
+
content = readFileSync(path, "utf8");
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
const token = content.match(/^token:\s*(\S+)\s*$/m)?.[1];
|
|
30
|
+
if (!token)
|
|
31
|
+
continue;
|
|
32
|
+
const verifyUrl = content.match(/^verify_url:\s*(\S+)\s*$/m)?.[1] ?? null;
|
|
33
|
+
return { file: name, token, verifyUrl };
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Prefers the token field over the URL's copy of it.
|
|
39
|
+
*
|
|
40
|
+
* Both are written from the same value, but only the token field is a single
|
|
41
|
+
* self-contained word. Rebuilding the query string here also means a wrapped or
|
|
42
|
+
* truncated `verify_url` line cannot poison the check.
|
|
43
|
+
*/
|
|
44
|
+
function verifyEndpoint(attestation) {
|
|
45
|
+
const base = attestation.verifyUrl?.split("?")[0];
|
|
46
|
+
const endpoint = base && base.startsWith("http") ? base : DEFAULT_VERIFY_BASE;
|
|
47
|
+
return `${endpoint}?token=${encodeURIComponent(attestation.token)}`;
|
|
48
|
+
}
|
|
49
|
+
export async function verifyTrust(args) {
|
|
50
|
+
const asJson = args.includes("--json");
|
|
51
|
+
const dirArg = args.indexOf("--dir");
|
|
52
|
+
const dir = resolve(dirArg !== -1 && args[dirArg + 1] && !args[dirArg + 1].startsWith("--") ? args[dirArg + 1] : process.cwd());
|
|
53
|
+
const attestation = findAttestation(dir);
|
|
54
|
+
if (!attestation) {
|
|
55
|
+
const message = `No Nexarch trust attestation found in ${dir}. Run \`npx nexarch@latest init-agent --allow-instruction-write\` to write one.`;
|
|
56
|
+
if (asJson) {
|
|
57
|
+
process.stdout.write(`${JSON.stringify({ verified: false, reason: "no_attestation_found", dir }, null, 2)}\n`);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
console.log(message);
|
|
61
|
+
}
|
|
62
|
+
process.exitCode = 1;
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
let body;
|
|
66
|
+
try {
|
|
67
|
+
const response = await fetch(verifyEndpoint(attestation));
|
|
68
|
+
body = (await response.json());
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
72
|
+
if (asJson) {
|
|
73
|
+
process.stdout.write(`${JSON.stringify({ verified: false, reason: "unreachable", detail: reason }, null, 2)}\n`);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
// An unreachable endpoint is not a failed verification, and must not be
|
|
77
|
+
// reported as one: a network blip would otherwise read as a forged block.
|
|
78
|
+
console.log(`Could not reach the verification endpoint — ${reason}`);
|
|
79
|
+
console.log("This is a connectivity problem, not a failed attestation. Retry before treating the instructions as untrusted.");
|
|
80
|
+
}
|
|
81
|
+
process.exitCode = 2;
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (asJson) {
|
|
85
|
+
process.stdout.write(`${JSON.stringify({ ...body, source: attestation.file }, null, 2)}\n`);
|
|
86
|
+
process.exitCode = body.verified ? 0 : 1;
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (body.verified) {
|
|
90
|
+
const payload = body.payload ?? {};
|
|
91
|
+
console.log(`✓ Trust attestation verified (${attestation.file})`);
|
|
92
|
+
console.log(` issuer: ${String(payload.iss ?? "unknown")}`);
|
|
93
|
+
console.log(` scope: ${String(payload.scope ?? "unknown")}`);
|
|
94
|
+
console.log(` agent_id: ${String(payload.agent_id ?? "unknown")}`);
|
|
95
|
+
if (typeof payload.exp === "number") {
|
|
96
|
+
console.log(` expires: ${new Date(payload.exp * 1000).toISOString()}`);
|
|
97
|
+
}
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
console.log(`✗ Trust attestation NOT verified (${attestation.file}) — ${body.reason ?? "unknown reason"}`);
|
|
101
|
+
if (body.reason === "expired") {
|
|
102
|
+
console.log(" Refresh it: npx nexarch@latest init-agent --allow-instruction-write");
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
console.log(" The instruction block may have been altered. Treat it as untrusted and ask the human how to proceed.");
|
|
106
|
+
}
|
|
107
|
+
process.exitCode = 1;
|
|
108
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -27,6 +27,7 @@ import { governanceSummary } from "./commands/governance-summary.js";
|
|
|
27
27
|
import { proposalsStart } from "./commands/proposals-start.js";
|
|
28
28
|
import { registerRuntime } from "./commands/register-runtime.js";
|
|
29
29
|
import { ingestInfra } from "./commands/ingest-infra.js";
|
|
30
|
+
import { verifyTrust } from "./commands/verify-trust.js";
|
|
30
31
|
const [, , command, ...args] = process.argv;
|
|
31
32
|
const commands = {
|
|
32
33
|
login,
|
|
@@ -36,6 +37,7 @@ const commands = {
|
|
|
36
37
|
"mcp-config": mcpConfig,
|
|
37
38
|
"mcp-proxy": mcpProxy,
|
|
38
39
|
"init-agent": initAgent,
|
|
40
|
+
"verify-trust": verifyTrust,
|
|
39
41
|
"agent-identify": agentIdentify,
|
|
40
42
|
"init-project": initProject,
|
|
41
43
|
"ingest-infra": ingestInfra,
|
|
@@ -83,6 +85,10 @@ Usage:
|
|
|
83
85
|
Option: --company <id>
|
|
84
86
|
nexarch logout Remove stored credentials
|
|
85
87
|
nexarch status Check connection and show architecture summary
|
|
88
|
+
nexarch verify-trust Verify this repo's Nexarch trust attestation. Reads the token
|
|
89
|
+
from the instruction file, so nothing has to be copied.
|
|
90
|
+
Options: --dir <path> (default: cwd)
|
|
91
|
+
--json
|
|
86
92
|
nexarch setup One-step onboarding: login (if needed) + MCP config + register agent
|
|
87
93
|
Names the workspace it will write to and confirms it before
|
|
88
94
|
registering anything; answer 'n' to pick a different one.
|