run402 4.16.1 → 4.17.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/README.md CHANGED
@@ -74,7 +74,7 @@ This links two separately held public identities through fresh signatures; it ne
74
74
 
75
75
  ### Buzz community control plane
76
76
 
77
- `run402 buzz status` reports skill installation, human adoption, community installation, and this agent's enrollment independently, and safely reports `supported: false` against an older gateway. Goal-shaped workflows are `buzz adopt`, `buzz install`, and `buzz enroll`; explicit decisions are `buzz adopt complete|cancel`, `buzz install activate|update|revoke`, and `buzz approve|deny|revoke`. `buzz install discover --community <buzz:community:host>` lists Run402-owned active descriptors without authentication. Activation alone accepts the ordinary Buzz kind-1 approval proof; updates and revocation are Run402-owner decisions and require no new Buzz signature. All print JSON to stdout, advice to stderr, have zero spend impact, and never accept Nostr/wallet secrets, session material, service keys, delegates, or payment credentials. Failure JSON preserves the gateway's code-specific `next_actions`, exact repair `field`, and retry safety; never replace it with a generic edit-and-retry. See the [Fizz/Honey examples](../buzz/references/community-control-plane.md).
77
+ `run402 buzz status` reports skill installation, human-adoption offers/adoptions, community installation, and this agent's enrollment independently, and safely reports `supported: false` against an older gateway. The canonical ownership workflow is `buzz adopt offer --org … --identity-link …`: it capability-checks before mutation and returns a normal durable HTTPS handoff. Poll with `buzz adopt offer show <buzzhao_id>` and cancel with `buzz adopt offer cancel <buzzhao_id>`. The raw challenge command is explicitly advanced as `buzz adopt direct …`; direct completion/cancellation remain `buzz adopt complete|cancel`. Other goal workflows are `buzz install` and `buzz enroll`; decisions are `buzz install activate|update|revoke` and `buzz approve|deny|revoke`. `buzz install discover --community <buzz:community:host>` lists Run402-owned active descriptors without authentication. MCP intentionally has no signing/passkey mutation and only renders exact HTTPS/CLI handoffs. All commands print JSON to stdout, advice to stderr, have zero spend impact, and never accept Nostr/wallet secrets, session material, service keys, delegates, or payment credentials. Failure JSON preserves the gateway's code-specific `next_actions`, exact repair `field`, and retry safety; never replace it with a generic edit-and-retry. See the [Fizz/Honey examples](../buzz/references/community-control-plane.md).
78
78
 
79
79
  ### Buy from an x402 URL
80
80
 
package/cli.mjs CHANGED
@@ -67,7 +67,7 @@ Commands:
67
67
  operator Operator (human/email) session — login, then overview across your wallets
68
68
  service Run402 service health and availability (status, health)
69
69
  cache Inspect and invalidate the SSR origin cache (inspect, invalidate)
70
- doctor Health and config diagnostics (machine-readable with --json)
70
+ doctor Health and config diagnostics (JSON by default; includes --buzz preflight)
71
71
  dev Run Astro dev with Run402 env + credentials in scope
72
72
  logs Fetch function logs by request id (--request-id req_...)
73
73
 
@@ -0,0 +1,97 @@
1
+ import { readFileSync } from "node:fs";
2
+
3
+ const CONTRACT_URL = new URL("../../buzz/fixtures/run402-buzz-doctor-v1-contract.json", import.meta.url);
4
+
5
+ export const BUZZ_DOCTOR_CONTRACT = deepFreeze(JSON.parse(readFileSync(CONTRACT_URL, "utf8")));
6
+ export const BUZZ_DOCTOR_CONTRACT_ID = BUZZ_DOCTOR_CONTRACT.contract_id;
7
+ export const BUZZ_DOCTOR_CHECK_ORDER = BUZZ_DOCTOR_CONTRACT.check_order;
8
+ export const BUZZ_DOCTOR_STATUSES = new Set(BUZZ_DOCTOR_CONTRACT.statuses);
9
+ export const BUZZ_DOCTOR_ACTION_SURFACES = new Set(BUZZ_DOCTOR_CONTRACT.next_action.surfaces);
10
+ export const BUZZ_DOCTOR_MAX_AGE_MS = BUZZ_DOCTOR_CONTRACT.freshness.max_age_seconds * 1000;
11
+
12
+ export function validateBuzzDoctorAction(action, { surface } = {}) {
13
+ if (!action || typeof action !== "object" || Array.isArray(action)) return "action_not_object";
14
+ for (const field of BUZZ_DOCTOR_CONTRACT.next_action.required_fields) {
15
+ if (!(field in action)) return `action_missing_${field}`;
16
+ }
17
+ if (!BUZZ_DOCTOR_ACTION_SURFACES.has(action.surface)) return "action_surface_invalid";
18
+ if (surface && action.surface !== surface) return "action_surface_mismatch";
19
+ if (typeof action.type !== "string" || action.type.length === 0) return "action_type_invalid";
20
+ if (typeof action.command !== "string" || action.command.length === 0) return "action_command_invalid";
21
+ if (typeof action.why !== "string" || action.why.length === 0) return "action_why_invalid";
22
+ for (const field of ["safe_to_auto_execute", "requires_approval", "destructive", "idempotent"]) {
23
+ if (typeof action[field] !== "boolean") return `action_${field}_invalid`;
24
+ }
25
+ if (!action.spend_impact || action.spend_impact.currency !== "USD" || action.spend_impact.max_amount !== "0") {
26
+ return "action_spend_impact_invalid";
27
+ }
28
+ if (action.surface === "shell") {
29
+ if (!Array.isArray(action.argv) || action.argv.length === 0 || action.argv.some((part) => typeof part !== "string")) {
30
+ return "action_argv_invalid";
31
+ }
32
+ } else {
33
+ for (const field of BUZZ_DOCTOR_CONTRACT.next_action.non_shell_forbidden_fields) {
34
+ if (field in action) return `action_${field}_forbidden`;
35
+ }
36
+ }
37
+ return null;
38
+ }
39
+
40
+ export function validateBuzzDoctorReport(report, {
41
+ expectedSubjectHex,
42
+ walletProfile,
43
+ nodeExecutable,
44
+ run402Executable,
45
+ relayOrigin,
46
+ now = Date.now(),
47
+ } = {}) {
48
+ if (!report || typeof report !== "object" || Array.isArray(report)) return { valid: false, reason: "report_not_object" };
49
+ if (report.contract_id !== BUZZ_DOCTOR_CONTRACT_ID || report.mode !== "buzz") return { valid: false, reason: "contract_mismatch" };
50
+ if (!Array.isArray(report.checks)) return { valid: false, reason: "checks_not_array" };
51
+ if (report.checks.length !== BUZZ_DOCTOR_CHECK_ORDER.length) return { valid: false, reason: "check_count_mismatch" };
52
+ for (let index = 0; index < BUZZ_DOCTOR_CHECK_ORDER.length; index += 1) {
53
+ const check = report.checks[index];
54
+ const expectedName = BUZZ_DOCTOR_CHECK_ORDER[index];
55
+ if (!check || check.name !== expectedName) return { valid: false, reason: "check_order_mismatch" };
56
+ if (!BUZZ_DOCTOR_STATUSES.has(check.status)) return { valid: false, reason: "check_status_invalid" };
57
+ if (check.status === "blocked" && !BUZZ_DOCTOR_CONTRACT.codes_by_check[expectedName]?.includes(check.code)) {
58
+ return { valid: false, reason: "check_code_invalid" };
59
+ }
60
+ const actionable = check.status === "blocked" || (check.status === "warning" && Array.isArray(check.next_actions));
61
+ if (actionable) {
62
+ if (!Array.isArray(check.next_actions) || check.next_actions.length !== 1) return { valid: false, reason: "action_cardinality_invalid" };
63
+ const actionReason = validateBuzzDoctorAction(check.next_actions[0]);
64
+ if (actionReason) return { valid: false, reason: actionReason };
65
+ } else if ("next_actions" in check) {
66
+ return { valid: false, reason: "passing_action_forbidden" };
67
+ }
68
+ }
69
+ const generatedAt = Date.parse(report.generated_at);
70
+ if (!Number.isFinite(generatedAt) || generatedAt > now || now - generatedAt > BUZZ_DOCTOR_MAX_AGE_MS) {
71
+ return { valid: false, reason: "report_stale" };
72
+ }
73
+ const binding = report.binding;
74
+ if (!binding || typeof binding !== "object") return { valid: false, reason: "binding_missing" };
75
+ const expected = {
76
+ contract_id: BUZZ_DOCTOR_CONTRACT_ID,
77
+ expected_subject_hex: expectedSubjectHex,
78
+ wallet_profile: walletProfile,
79
+ node_executable: nodeExecutable,
80
+ run402_executable: run402Executable,
81
+ relay_origin: relayOrigin,
82
+ };
83
+ for (const field of BUZZ_DOCTOR_CONTRACT.freshness.binding_fields) {
84
+ if (expected[field] !== undefined && binding[field] !== expected[field]) return { valid: false, reason: `binding_${field}_mismatch` };
85
+ }
86
+ const computedOk = report.checks.every((check) => check.status !== "blocked");
87
+ if (report.ok !== computedOk) return { valid: false, reason: "verdict_mismatch" };
88
+ if (report.mutation_state !== BUZZ_DOCTOR_CONTRACT.zero_mutation.mutation_state) return { valid: false, reason: "mutation_state_invalid" };
89
+ return { valid: true, reason: null };
90
+ }
91
+
92
+ function deepFreeze(value) {
93
+ if (!value || typeof value !== "object" || Object.isFrozen(value)) return value;
94
+ Object.freeze(value);
95
+ for (const child of Object.values(value)) deepFreeze(child);
96
+ return value;
97
+ }
@@ -0,0 +1,127 @@
1
+ import assert from "node:assert/strict";
2
+ import { readFileSync } from "node:fs";
3
+ import { describe, it } from "node:test";
4
+ import {
5
+ BUZZ_DOCTOR_CHECK_ORDER,
6
+ BUZZ_DOCTOR_CONTRACT,
7
+ BUZZ_DOCTOR_CONTRACT_ID,
8
+ validateBuzzDoctorAction,
9
+ validateBuzzDoctorReport,
10
+ } from "./buzz-doctor-contract.mjs";
11
+
12
+ const FIXTURE = JSON.parse(readFileSync(new URL("../../buzz/fixtures/buzz-v0.5.2-cli-capabilities.json", import.meta.url), "utf8"));
13
+
14
+ function shellAction() {
15
+ return {
16
+ type: "upgrade_client",
17
+ surface: "shell",
18
+ command: "npm install -g run402@latest",
19
+ argv: ["npm", "install", "-g", "run402@latest"],
20
+ why: "Install the compatible Run402 CLI.",
21
+ safe_to_auto_execute: true,
22
+ requires_approval: false,
23
+ destructive: false,
24
+ idempotent: true,
25
+ spend_impact: { currency: "USD", max_amount: "0" },
26
+ };
27
+ }
28
+
29
+ function passingReport(now) {
30
+ return {
31
+ ok: true,
32
+ mode: "buzz",
33
+ contract_id: BUZZ_DOCTOR_CONTRACT_ID,
34
+ generated_at: new Date(now).toISOString(),
35
+ mutation_state: "not_started",
36
+ binding: {
37
+ contract_id: BUZZ_DOCTOR_CONTRACT_ID,
38
+ expected_subject_hex: "a".repeat(64),
39
+ wallet_profile: "buzz-fizz",
40
+ node_executable: "/usr/local/bin/node",
41
+ run402_executable: "/usr/local/bin/run402",
42
+ relay_origin: "wss://community.example",
43
+ },
44
+ checks: BUZZ_DOCTOR_CHECK_ORDER.map((name) => ({ name, status: "ok" })),
45
+ telemetry: { status: "disabled" },
46
+ };
47
+ }
48
+
49
+ describe("Buzz doctor v1 frozen contract", () => {
50
+ it("freezes the ordered checks, flags, statuses, exit streams, freshness binding, and zero-mutation verdict", () => {
51
+ assert.equal(BUZZ_DOCTOR_CONTRACT_ID, "run402.buzz-doctor.v1");
52
+ assert.deepEqual(BUZZ_DOCTOR_CHECK_ORDER, [
53
+ "session_shell", "node_runtime", "run402_cli", "buzz_cli", "buzz_agent_target",
54
+ "run402_api", "run402_console", "buzz_relay", "wallet_profile",
55
+ ]);
56
+ assert.deepEqual(BUZZ_DOCTOR_CONTRACT.statuses, ["ok", "warning", "blocked"]);
57
+ assert.deepEqual(BUZZ_DOCTOR_CONTRACT.flags, { mode: "--buzz", agent: "--buzz-agent", profile: "--wallet" });
58
+ assert.deepEqual(BUZZ_DOCTOR_CONTRACT.exit_behavior, {
59
+ passed_or_warning_only: 0,
60
+ completed_with_blocked_checks: 1,
61
+ usage_error: 1,
62
+ completed_report_stream: "stdout",
63
+ usage_error_stream: "stderr",
64
+ });
65
+ assert.equal(BUZZ_DOCTOR_CONTRACT.freshness.max_age_seconds, 60);
66
+ assert.deepEqual(BUZZ_DOCTOR_CONTRACT.setup_rejection_codes, [
67
+ "BUZZ_PREFLIGHT_REPORT_INVALID",
68
+ "BUZZ_PREFLIGHT_REPORT_STALE",
69
+ "BUZZ_PREFLIGHT_REPORT_MISMATCH",
70
+ ]);
71
+ assert.equal(BUZZ_DOCTOR_CONTRACT.zero_mutation.mutation_state, "not_started");
72
+ assert.ok(BUZZ_DOCTOR_CONTRACT.zero_mutation.forbidden.includes("buzz_event_publish"));
73
+ assert.ok(BUZZ_DOCTOR_CONTRACT.zero_mutation.forbidden.includes("run402_identity_link_mutation"));
74
+ });
75
+
76
+ it("requires exactly one complete destination-specific action on every actionable check", () => {
77
+ assert.equal(validateBuzzDoctorAction(shellAction()), null);
78
+ assert.equal(validateBuzzDoctorAction({ ...shellAction(), argv: undefined }), "action_argv_invalid");
79
+ const chat = { ...shellAction(), surface: "buzz_chat", command: "@Fizz restart setup" };
80
+ delete chat.argv;
81
+ assert.equal(validateBuzzDoctorAction(chat), null);
82
+ assert.equal(validateBuzzDoctorAction({ ...chat, argv: ["echo", "wrong"] }), "action_argv_forbidden");
83
+ });
84
+
85
+ it("rejects stale, reordered, mismatched, edited, or verdict-inconsistent reports", () => {
86
+ const now = Date.parse("2026-07-31T12:00:00.000Z");
87
+ const expected = {
88
+ expectedSubjectHex: "a".repeat(64),
89
+ walletProfile: "buzz-fizz",
90
+ nodeExecutable: "/usr/local/bin/node",
91
+ run402Executable: "/usr/local/bin/run402",
92
+ relayOrigin: "wss://community.example",
93
+ now,
94
+ };
95
+ assert.deepEqual(validateBuzzDoctorReport(passingReport(now), expected), { valid: true, reason: null });
96
+ assert.equal(validateBuzzDoctorReport(passingReport(now - 61_000), expected).reason, "report_stale");
97
+ const reordered = passingReport(now);
98
+ reordered.checks.reverse();
99
+ assert.equal(validateBuzzDoctorReport(reordered, expected).reason, "check_order_mismatch");
100
+ assert.equal(validateBuzzDoctorReport(passingReport(now), { ...expected, walletProfile: "buzz-honey" }).reason, "binding_wallet_profile_mismatch");
101
+ const edited = passingReport(now);
102
+ edited.checks[0] = { name: "session_shell", status: "blocked", code: "MADE_UP", next_actions: [shellAction()] };
103
+ assert.equal(validateBuzzDoctorReport(edited, expected).reason, "check_code_invalid");
104
+ const falseVerdict = passingReport(now);
105
+ falseVerdict.ok = false;
106
+ assert.equal(validateBuzzDoctorReport(falseVerdict, expected).reason, "verdict_mismatch");
107
+ });
108
+
109
+ it("freezes released Buzz v0.5.2 as capability-first with JSON outputs and no version flag", () => {
110
+ assert.equal(FIXTURE.fixture_id, "buzz-cli-v0.5.2-capabilities");
111
+ assert.equal(FIXTURE.buzz_release.release_tag_commit, "3e48f1b2365d326ee1c9582448d86a99b44ecd5d");
112
+ assert.deepEqual(FIXTURE.help_probes.map((probe) => probe.argv.slice(1)), [
113
+ ["--help"],
114
+ ["users", "get", "--help"],
115
+ ["social", "publish", "--help"],
116
+ ["social", "event", "--help"],
117
+ ]);
118
+ assert.equal(FIXTURE.version_probe.supported, false);
119
+ assert.equal(FIXTURE.version_probe.stderr_json.error, "user_error");
120
+ assert.equal(FIXTURE.json_contract.default_output_format, "json");
121
+ assert.equal(FIXTURE.json_contract.users_get_success_stdout.type, "array");
122
+ assert.equal(FIXTURE.managed_sidecar.public_package_manager_install, false);
123
+ assert.equal(FIXTURE.managed_sidecar.repair_surface, "buzz_settings");
124
+ assert.equal(FIXTURE.public_self_observation.requires_private_key_value_inspection, false);
125
+ assert.equal(FIXTURE.write_surface_policy.publish_is_never_invoked_by_doctor, true);
126
+ });
127
+ });