From 47a63a0288ebd975a2287cf76b878e68a20f2565 Mon Sep 17 00:00:00 2001 From: David Abutbul Date: Thu, 23 Jul 2026 22:31:19 +0300 Subject: [PATCH] feat(contracts): add result envelope v1 --- contracts/exit-codes.json | 39 + ...tor-error-unknown-version-nanoclaw-v2.json | 31 + .../valid/core-doctor-pass-nanoclaw-v2.json | 31 + contracts/result-operation-policy.json | 245 +++ contracts/result-outcomes.json | 38 + contracts/result-reason-codes.json | 56 + .../schemas/result/result-v1.schema.json | 322 +++ package.json | 1 + .../ci/validate_clawsec_result_envelope.mjs | 1804 +++++++++++++++++ .../test-skill-result-envelope-contract.mjs | 1146 +++++++++++ 10 files changed, 3713 insertions(+) create mode 100644 contracts/exit-codes.json create mode 100644 contracts/fixtures/result-v1/valid/core-doctor-error-unknown-version-nanoclaw-v2.json create mode 100644 contracts/fixtures/result-v1/valid/core-doctor-pass-nanoclaw-v2.json create mode 100644 contracts/result-operation-policy.json create mode 100644 contracts/result-outcomes.json create mode 100644 contracts/result-reason-codes.json create mode 100644 contracts/schemas/result/result-v1.schema.json create mode 100644 scripts/ci/validate_clawsec_result_envelope.mjs create mode 100644 scripts/test-skill-result-envelope-contract.mjs diff --git a/contracts/exit-codes.json b/contracts/exit-codes.json new file mode 100644 index 00000000..2a5b901f --- /dev/null +++ b/contracts/exit-codes.json @@ -0,0 +1,39 @@ +{ + "contract": "clawsec.cli-exit-codes/v1", + "contract_version": "1", + "passthrough_child_exit_codes": false, + "entries": [ + { + "code": 0, + "outcome": "pass" + }, + { + "code": 1, + "outcome": "error" + }, + { + "code": 2, + "outcome": "finding" + }, + { + "code": 3, + "outcome": "blocked" + }, + { + "code": 4, + "outcome": "degraded" + }, + { + "code": 5, + "outcome": "unsupported" + }, + { + "code": 6, + "outcome": "not_applicable" + }, + { + "code": 42, + "outcome": "confirmation_required" + } + ] +} diff --git a/contracts/fixtures/result-v1/valid/core-doctor-error-unknown-version-nanoclaw-v2.json b/contracts/fixtures/result-v1/valid/core-doctor-error-unknown-version-nanoclaw-v2.json new file mode 100644 index 00000000..96c1702f --- /dev/null +++ b/contracts/fixtures/result-v1/valid/core-doctor-error-unknown-version-nanoclaw-v2.json @@ -0,0 +1,31 @@ +{ + "schema": "clawsec.result/v1", + "executor": { + "schema": "clawsec.component-ref/v1", + "name": "clawsec-core-nanoclaw", + "version": "0.1.0-rc.1", + "harness": "nanoclaw", + "role": "core", + "metadata_digest": "sha256:765f343f11e9367051b5ed95daed35ff318945c672df0f5509c0dbcd1276eea5" + }, + "subject": { + "kind": "executor" + }, + "invocation": { + "id": "11111111-1111-4111-8111-111111111111", + "operation": "core.doctor", + "harness": { + "name": "nanoclaw", + "version": null + }, + "scope": { + "kind": "nanoclaw.checkout", + "ref": "nanoclaw-v2-lab-01" + } + }, + "reported_at": "2026-07-23T10:00:00Z", + "outcome": "error", + "reason_code": "internal_failure", + "summary": "NanoClaw version detection failed before doctor checks could complete.", + "effects": [] +} diff --git a/contracts/fixtures/result-v1/valid/core-doctor-pass-nanoclaw-v2.json b/contracts/fixtures/result-v1/valid/core-doctor-pass-nanoclaw-v2.json new file mode 100644 index 00000000..428fd104 --- /dev/null +++ b/contracts/fixtures/result-v1/valid/core-doctor-pass-nanoclaw-v2.json @@ -0,0 +1,31 @@ +{ + "schema": "clawsec.result/v1", + "executor": { + "schema": "clawsec.component-ref/v1", + "name": "clawsec-core-nanoclaw", + "version": "0.1.0-rc.1", + "harness": "nanoclaw", + "role": "core", + "metadata_digest": "sha256:765f343f11e9367051b5ed95daed35ff318945c672df0f5509c0dbcd1276eea5" + }, + "subject": { + "kind": "executor" + }, + "invocation": { + "id": "22222222-2222-4222-8222-222222222222", + "operation": "core.doctor", + "harness": { + "name": "nanoclaw", + "version": "2.1.17" + }, + "scope": { + "kind": "nanoclaw.checkout", + "ref": "nanoclaw-v2-lab-01" + } + }, + "reported_at": "2026-07-23T10:01:00Z", + "outcome": "pass", + "reason_code": "operation_completed", + "summary": "Synthetic contract fixture: the executor reports NanoClaw core doctor completion.", + "effects": [] +} diff --git a/contracts/result-operation-policy.json b/contracts/result-operation-policy.json new file mode 100644 index 00000000..2c5d0fac --- /dev/null +++ b/contracts/result-operation-policy.json @@ -0,0 +1,245 @@ +{ + "contract": "clawsec.result-operation-policy/v1", + "contract_version": "1", + "operations": { + "core.doctor": { + "effect_class": "read_only", + "subject_kinds": [ + "executor" + ] + }, + "core.evaluate-advisories": { + "effect_class": "read_only", + "subject_kinds": [ + "scope" + ] + }, + "core.install-release": { + "effect_class": "mutation", + "subject_kinds": [ + "component" + ] + }, + "core.inventory": { + "effect_class": "read_only", + "subject_kinds": [ + "scope" + ] + }, + "core.plan-release": { + "effect_class": "planning", + "subject_kinds": [ + "component" + ] + }, + "core.remove-release": { + "effect_class": "mutation", + "subject_kinds": [ + "component" + ] + }, + "core.update-release": { + "effect_class": "mutation", + "subject_kinds": [ + "component" + ] + }, + "core.verify-feed": { + "effect_class": "read_only", + "subject_kinds": [ + "scope" + ] + }, + "core.verify-receipt": { + "effect_class": "read_only", + "subject_kinds": [ + "component" + ] + }, + "core.verify-release": { + "effect_class": "read_only", + "subject_kinds": [ + "component", + "scope" + ] + }, + "guardian.status": { + "effect_class": "read_only", + "subject_kinds": [ + "executor" + ] + }, + "posture.diff": { + "effect_class": "read_only", + "subject_kinds": [ + "scope" + ] + }, + "posture.snapshot": { + "effect_class": "read_only", + "subject_kinds": [ + "scope" + ] + }, + "posture.verify": { + "effect_class": "read_only", + "subject_kinds": [ + "scope" + ] + }, + "suite.catalog": { + "effect_class": "read_only", + "subject_kinds": [ + "scope" + ] + }, + "suite.disable": { + "effect_class": "mutation", + "subject_kinds": [ + "component" + ] + }, + "suite.doctor": { + "effect_class": "read_only", + "subject_kinds": [ + "executor" + ] + }, + "suite.enable": { + "effect_class": "mutation", + "subject_kinds": [ + "component" + ] + }, + "suite.install": { + "effect_class": "mutation", + "subject_kinds": [ + "component" + ] + }, + "suite.recommend": { + "effect_class": "planning", + "subject_kinds": [ + "scope" + ] + }, + "suite.recurring-advisory-verification": { + "effect_class": "mutation", + "subject_kinds": [ + "scope" + ] + }, + "suite.status": { + "effect_class": "read_only", + "subject_kinds": [ + "scope" + ] + } + }, + "effect_classes": { + "mutation": { + "blocked": { + "allowed_states": [ + "proposed" + ], + "minimum_count": 0 + }, + "confirmation_required": { + "allowed_states": [ + "proposed" + ], + "minimum_count": 1 + }, + "degraded": null, + "error": { + "allowed_states": [ + "applied", + "failed", + "proposed", + "rolled_back" + ], + "minimum_count": 0 + }, + "finding": null, + "not_applicable": { + "allowed_states": [], + "minimum_count": 0 + }, + "pass": { + "allowed_states": [ + "applied" + ], + "minimum_count": 1 + }, + "unsupported": { + "allowed_states": [], + "minimum_count": 0 + } + }, + "planning": { + "blocked": { + "allowed_states": [ + "proposed" + ], + "minimum_count": 0 + }, + "confirmation_required": null, + "degraded": { + "allowed_states": [ + "proposed" + ], + "minimum_count": 0 + }, + "error": { + "allowed_states": [], + "minimum_count": 0 + }, + "finding": null, + "not_applicable": { + "allowed_states": [], + "minimum_count": 0 + }, + "pass": { + "allowed_states": [ + "proposed" + ], + "minimum_count": 1 + }, + "unsupported": { + "allowed_states": [], + "minimum_count": 0 + } + }, + "read_only": { + "blocked": { + "allowed_states": [], + "minimum_count": 0 + }, + "confirmation_required": null, + "degraded": { + "allowed_states": [], + "minimum_count": 0 + }, + "error": { + "allowed_states": [], + "minimum_count": 0 + }, + "finding": { + "allowed_states": [], + "minimum_count": 0 + }, + "not_applicable": { + "allowed_states": [], + "minimum_count": 0 + }, + "pass": { + "allowed_states": [], + "minimum_count": 0 + }, + "unsupported": { + "allowed_states": [], + "minimum_count": 0 + } + } + } +} diff --git a/contracts/result-outcomes.json b/contracts/result-outcomes.json new file mode 100644 index 00000000..d90e3d59 --- /dev/null +++ b/contracts/result-outcomes.json @@ -0,0 +1,38 @@ +{ + "contract": "clawsec.result-outcomes/v1", + "contract_version": "1", + "outcomes": [ + { + "name": "blocked", + "description": "The executor reports that policy or a required prerequisite prevented completion." + }, + { + "name": "confirmation_required", + "description": "The executor reports that it stopped before mutation and needs explicit operator confirmation; authorization requires a typed plan result." + }, + { + "name": "degraded", + "description": "The executor reports useful information with weaker assurance or coverage than intended." + }, + { + "name": "error", + "description": "The executor reports that execution failed because of invalid input or an internal failure." + }, + { + "name": "finding", + "description": "The executor reports that the operation completed and found a security-relevant condition." + }, + { + "name": "not_applicable", + "description": "The executor reports that the operation is valid but unnecessary for the current subject." + }, + { + "name": "pass", + "description": "The executor reports that the operation completed as intended; this outcome alone is not proof of verification or mutation." + }, + { + "name": "unsupported", + "description": "The executor reports that a declared operation cannot be provided for the current harness or scope." + } + ] +} diff --git a/contracts/result-reason-codes.json b/contracts/result-reason-codes.json new file mode 100644 index 00000000..66d62369 --- /dev/null +++ b/contracts/result-reason-codes.json @@ -0,0 +1,56 @@ +{ + "contract": "clawsec.result-reason-codes/v1", + "contract_version": "1", + "reasons": [ + { + "code": "assurance_unavailable", + "outcome": "degraded", + "description": "Required assurance or coverage was unavailable, but a bounded result was still produced." + }, + { + "code": "intentionally_unsupported", + "outcome": "unsupported", + "description": "The executor declares the operation but reports it unavailable for the current harness instance or scope." + }, + { + "code": "internal_failure", + "outcome": "error", + "description": "The executor failed unexpectedly." + }, + { + "code": "invalid_input", + "outcome": "error", + "description": "The operation input could not be parsed or validated." + }, + { + "code": "operation_completed", + "outcome": "pass", + "description": "The executor reports that the operation completed as intended; proof-bearing claims require a typed result." + }, + { + "code": "operation_not_applicable", + "outcome": "not_applicable", + "description": "The operation is valid but unnecessary for the current subject." + }, + { + "code": "policy_blocked", + "outcome": "blocked", + "description": "A security or operator policy refused the operation." + }, + { + "code": "prerequisite_missing", + "outcome": "blocked", + "description": "A required declared prerequisite was absent or incompatible." + }, + { + "code": "security_finding_detected", + "outcome": "finding", + "description": "The operation completed and found a security-relevant condition." + }, + { + "code": "user_confirmation_required", + "outcome": "confirmation_required", + "description": "Explicit operator confirmation is required before mutation." + } + ] +} diff --git a/contracts/schemas/result/result-v1.schema.json b/contracts/schemas/result/result-v1.schema.json new file mode 100644 index 00000000..5a3fd733 --- /dev/null +++ b/contracts/schemas/result/result-v1.schema.json @@ -0,0 +1,322 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://clawsec.prompt.security/contracts/schemas/result/result-v1.schema.json", + "title": "ClawSec base operation result v1", + "description": "Closed harness-neutral execution summary. Typed advisory, composition, plan, and receipt contracts contain this base result when they need proof-bearing details.", + "type": "object", + "additionalProperties": false, + "required": [ + "schema", + "executor", + "subject", + "invocation", + "reported_at", + "outcome", + "reason_code", + "summary", + "effects" + ], + "properties": { + "schema": { + "const": "clawsec.result/v1" + }, + "executor": { + "$ref": "https://clawsec.prompt.security/contracts/schemas/component/component-ref-v1.schema.json" + }, + "subject": { + "$ref": "#/definitions/subject" + }, + "invocation": { + "$ref": "#/definitions/invocation" + }, + "reported_at": { + "$ref": "#/definitions/timestamp" + }, + "outcome": { + "$ref": "#/definitions/outcome" + }, + "reason_code": { + "$ref": "#/definitions/reasonCode" + }, + "summary": { + "$ref": "#/definitions/summary" + }, + "effects": { + "description": "Bounded executor-reported summaries. Detailed plan, receipt, and ownership records belong in typed contracts.", + "type": "array", + "maxItems": 64, + "items": { + "$ref": "#/definitions/effect" + } + } + }, + "definitions": { + "capabilityId": { + "type": "string", + "pattern": "^(?:core|suite|guardian|posture)\\.[a-z][a-z0-9]*(?:-[a-z0-9]+)*$" + }, + "invocationId": { + "description": "Lowercase UUID v4 used only to correlate this invocation. It does not prove freshness, authorization, or a parent-child relationship.", + "type": "string", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + }, + "timestamp": { + "description": "UTC RFC 3339 timestamp.", + "type": "string", + "format": "date-time", + "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?Z$" + }, + "outcome": { + "enum": [ + "pass", + "finding", + "confirmation_required", + "blocked", + "degraded", + "unsupported", + "not_applicable", + "error" + ] + }, + "reasonCode": { + "type": "string", + "pattern": "^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$" + }, + "summary": { + "type": "string", + "minLength": 1, + "maxLength": 512, + "pattern": "^[^\\u0000-\\u001f\\u007f]+$" + }, + "harness": { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "version" + ], + "properties": { + "name": { + "enum": [ + "openclaw", + "hermes", + "nanoclaw", + "picoclaw" + ] + }, + "version": { + "description": "Actual detected harness version, or null only when the result cannot safely identify it.", + "oneOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "null" + } + ] + } + } + }, + "scope": { + "type": "object", + "additionalProperties": false, + "required": [ + "kind" + ], + "properties": { + "kind": { + "enum": [ + "host", + "openclaw.workspace", + "openclaw.project-agent", + "openclaw.personal-agent", + "openclaw.managed", + "openclaw.global", + "hermes.profile", + "nanoclaw.checkout", + "picoclaw.home", + "picoclaw.workspace", + "picoclaw.global" + ] + }, + "ref": { + "description": "Opaque non-secret scope identifier, never an absolute filesystem path or URL.", + "type": "string", + "minLength": 1, + "maxLength": 128, + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:@+-]*$" + } + }, + "oneOf": [ + { + "properties": { + "kind": { + "const": "host" + } + }, + "not": { + "required": [ + "ref" + ] + } + }, + { + "properties": { + "kind": { + "not": { + "const": "host" + } + } + }, + "required": [ + "ref" + ] + } + ] + }, + "invocation": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "operation", + "harness", + "scope" + ], + "properties": { + "id": { + "$ref": "#/definitions/invocationId" + }, + "operation": { + "$ref": "#/definitions/capabilityId" + }, + "harness": { + "$ref": "#/definitions/harness" + }, + "scope": { + "$ref": "#/definitions/scope" + } + } + }, + "subject": { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": [ + "kind" + ], + "properties": { + "kind": { + "const": "executor" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "kind" + ], + "properties": { + "kind": { + "const": "scope" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "kind", + "component" + ], + "properties": { + "kind": { + "const": "component" + }, + "component": { + "$ref": "https://clawsec.prompt.security/contracts/schemas/component/component-ref-v1.schema.json" + } + } + } + ] + }, + "effect": { + "description": "Executor-reported effect summary. It is not a plan, receipt, ownership record, or proof that the state change occurred.", + "type": "object", + "additionalProperties": false, + "required": [ + "effect_id", + "state", + "target", + "summary" + ], + "properties": { + "effect_id": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "pattern": "^[a-z0-9][a-z0-9._-]*$" + }, + "state": { + "enum": [ + "proposed", + "applied", + "failed", + "rolled_back" + ] + }, + "target": { + "$ref": "#/definitions/effectTarget" + }, + "summary": { + "$ref": "#/definitions/summary" + } + } + }, + "effectTarget": { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": [ + "kind", + "value" + ], + "properties": { + "kind": { + "const": "relative_path" + }, + "value": { + "type": "string", + "minLength": 1, + "maxLength": 512, + "pattern": "^(?!.*(?:^|/)\\.\\.?(?:/|$))[A-Za-z0-9._@+-]+(?:/[A-Za-z0-9._@+-]+)*$" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "kind", + "value" + ], + "properties": { + "kind": { + "const": "harness_resource" + }, + "value": { + "type": "string", + "minLength": 1, + "maxLength": 512, + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:@+-]{0,511}$" + } + } + } + ] + } + } +} diff --git a/package.json b/package.json index 678ffe0f..ee02f827 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "scripts": { "gen:wiki-llms": "node scripts/generate-wiki-llms.mjs", "test:metadata-contracts": "node scripts/test-skill-component-metadata.mjs", + "test:result-envelope-contract": "node scripts/test-skill-result-envelope-contract.mjs", "populate-local-wiki": "./scripts/populate-local-wiki.sh", "predev": "npm run gen:wiki-llms", "dev": "vite", diff --git a/scripts/ci/validate_clawsec_result_envelope.mjs b/scripts/ci/validate_clawsec_result_envelope.mjs new file mode 100644 index 00000000..b5e8cdae --- /dev/null +++ b/scripts/ci/validate_clawsec_result_envelope.mjs @@ -0,0 +1,1804 @@ +#!/usr/bin/env node + +import { createHash } from "node:crypto"; +import { readFile } from "node:fs/promises"; +import path from "node:path"; +import { isDeepStrictEqual, TextDecoder } from "node:util"; +import { fileURLToPath, pathToFileURL } from "node:url"; + +import Ajv from "ajv"; + +import { + compareSemverV2, + parseSemverV2, +} from "./lifecycle_semver.mjs"; + +const repositoryRoot = fileURLToPath(new URL("../../", import.meta.url)); +const RESULT_MAXIMUM_BYTES = 1024 * 1024; +const RESULT_MAXIMUM_NESTING_DEPTH = 64; +const MAXIMUM_DIAGNOSTICS = 64; +const diagnosticKeysByArray = new WeakMap(); +const resultSchemaPath = path.join( + repositoryRoot, + "contracts/schemas/result/result-v1.schema.json", +); +const componentRefSchemaPath = path.join( + repositoryRoot, + "contracts/schemas/component/component-ref-v1.schema.json", +); +const capabilityRegistryPath = path.join( + repositoryRoot, + "contracts/capability-registry.json", +); +const outcomeRegistryPath = path.join(repositoryRoot, "contracts/result-outcomes.json"); +const reasonRegistryPath = path.join(repositoryRoot, "contracts/result-reason-codes.json"); +const exitCodeRegistryPath = path.join(repositoryRoot, "contracts/exit-codes.json"); +const operationPolicyPath = path.join( + repositoryRoot, + "contracts/result-operation-policy.json", +); + +const loadedContracts = await Promise.all([ + readContractJson( + resultSchemaPath, + "RESULT_SCHEMA_SOURCE", + "/contracts/schemas/result/result-v1.schema.json", + ), + readContractJson( + componentRefSchemaPath, + "COMPONENT_REF_SCHEMA_SOURCE", + "/contracts/schemas/component/component-ref-v1.schema.json", + ), + readContractJson( + capabilityRegistryPath, + "CAPABILITY_REGISTRY_SOURCE", + "/contracts/capability-registry.json", + ), + readContractJson( + outcomeRegistryPath, + "RESULT_OUTCOME_REGISTRY_SOURCE", + "/contracts/result-outcomes.json", + ), + readContractJson( + reasonRegistryPath, + "RESULT_REASON_REGISTRY_SOURCE", + "/contracts/result-reason-codes.json", + ), + readContractJson( + exitCodeRegistryPath, + "RESULT_EXIT_REGISTRY_SOURCE", + "/contracts/exit-codes.json", + ), + readContractJson( + operationPolicyPath, + "RESULT_OPERATION_POLICY_SOURCE", + "/contracts/result-operation-policy.json", + ), +]); + +const initializationErrors = loadedContracts.flatMap((entry) => entry.errors); +const [ + resultSchema, + componentRefSchema, + capabilityRegistry, + outcomeRegistry, + reasonRegistry, + exitCodeRegistry, + operationPolicy, +] = loadedContracts.map((entry) => entry.value ?? {}); + +let validateClawsecMetadata = null; +let validateComponentRef = null; +if (initializationErrors.length === 0) { + try { + const metadataValidator = await import("./validate_clawsec_metadata.mjs"); + validateClawsecMetadata = metadataValidator.validateClawsecMetadata; + validateComponentRef = metadataValidator.validateComponentRef; + } catch { + addError( + initializationErrors, + "RESULT_METADATA_VALIDATOR_LOAD_FAILED", + "/scripts/ci/validate_clawsec_metadata.mjs", + "metadata validator could not be loaded", + ); + } +} + +let validateResultSchema = null; +if (initializationErrors.length === 0) { + try { + const ajv = new Ajv({ + allErrors: true, + jsonPointers: true, + schemaId: "auto", + }); + ajv.addSchema(componentRefSchema); + validateResultSchema = ajv.compile(resultSchema); + } catch { + addError( + initializationErrors, + "RESULT_SCHEMA_COMPILE_FAILED", + "/contracts/schemas/result/result-v1.schema.json", + "result schema could not be compiled with its registered component schema", + ); + } +} + +const EXPECTED_OUTCOMES = Object.freeze([ + "blocked", + "confirmation_required", + "degraded", + "error", + "finding", + "not_applicable", + "pass", + "unsupported", +]); +const EXPECTED_EXIT_CODES = Object.freeze({ + blocked: 3, + confirmation_required: 42, + degraded: 4, + error: 1, + finding: 2, + not_applicable: 6, + pass: 0, + unsupported: 5, +}); +const NULL_VERSION_OUTCOMES = new Set(["blocked", "error"]); +const SAFE_REFUSAL_OUTCOMES = new Set(["blocked", "error", "unsupported"]); +const NANOCLAW_CHECKOUT_OPERATIONS = new Set([ + "core.install-release", + "core.plan-release", + "core.remove-release", + "core.update-release", + "suite.disable", + "suite.enable", + "suite.install", + "suite.recurring-advisory-verification", +]); +const NANOCLAW_PACKAGE_TREE_OPERATIONS = new Set([ + "core.install-release", + "core.plan-release", + "core.remove-release", + "core.update-release", + "suite.install", +]); +const capabilityByName = new Map( + (Array.isArray(capabilityRegistry.capabilities) + ? capabilityRegistry.capabilities + : []).map((entry) => [entry.name, entry]), +); +const reasonByCode = new Map( + (Array.isArray(reasonRegistry.reasons) + ? reasonRegistry.reasons + : []).map((entry) => [entry.code, entry]), +); +const exitCodeByOutcome = new Map( + (Array.isArray(exitCodeRegistry.entries) + ? exitCodeRegistry.entries + : []).map((entry) => [entry.outcome, entry.code]), +); + +export function validateResultEnvelopeRegistries() { + const errors = [...initializationErrors]; + if (errors.length > 0) return finish(errors); + + validateClosedObject( + capabilityRegistry, + ["capabilities", "contract", "contract_version"], + "/contracts/capability-registry.json", + errors, + ); + validateClosedObject( + outcomeRegistry, + ["contract", "contract_version", "outcomes"], + "/contracts/result-outcomes.json", + errors, + ); + validateClosedObject( + reasonRegistry, + ["contract", "contract_version", "reasons"], + "/contracts/result-reason-codes.json", + errors, + ); + validateClosedObject( + exitCodeRegistry, + ["contract", "contract_version", "entries", "passthrough_child_exit_codes"], + "/contracts/exit-codes.json", + errors, + ); + validateClosedObject( + operationPolicy, + ["contract", "contract_version", "effect_classes", "operations"], + "/contracts/result-operation-policy.json", + errors, + ); + validateRegistryHeader( + capabilityRegistry, + "clawsec.capability-registry/v1", + "/contracts/capability-registry.json", + errors, + ); + validateRegistryHeader( + outcomeRegistry, + "clawsec.result-outcomes/v1", + "/contracts/result-outcomes.json", + errors, + ); + validateRegistryHeader( + reasonRegistry, + "clawsec.result-reason-codes/v1", + "/contracts/result-reason-codes.json", + errors, + ); + validateRegistryHeader( + exitCodeRegistry, + "clawsec.cli-exit-codes/v1", + "/contracts/exit-codes.json", + errors, + ); + validateRegistryHeader( + operationPolicy, + "clawsec.result-operation-policy/v1", + "/contracts/result-operation-policy.json", + errors, + ); + + validateCapabilityRegistry(errors); + const outcomes = validateOutcomeRegistry(errors); + validateReasonRegistry(outcomes, errors); + validateExitCodeRegistry(outcomes, errors); + validateOperationPolicyRegistry(outcomes, errors); + + const schemaOutcomes = resultSchema?.definitions?.outcome?.enum; + if ( + !Array.isArray(schemaOutcomes) + || !sameStringSet(schemaOutcomes, EXPECTED_OUTCOMES) + ) { + addError( + errors, + "RESULT_SCHEMA_OUTCOMES_MISMATCH", + "/contracts/schemas/result/result-v1.schema.json/definitions/outcome/enum", + "result schema outcomes must exactly match the result outcome registry", + ); + } + + return finish(errors); +} + +export function validateUnboundResultEnvelope(document) { + const registryResult = validateResultEnvelopeRegistries(); + const errors = [...registryResult.errors]; + if (!registryResult.valid || validateResultSchema === null) { + return finishStructure(errors); + } + + if (!validateResultSchema(document)) { + for (const schemaError of validateResultSchema.errors ?? []) { + const additionalProperty = schemaError.keyword === "additionalProperties" + ? schemaError.params?.additionalProperty + : null; + const errorPath = additionalProperty + ? joinJsonPointer(schemaError.dataPath || "", additionalProperty) + : schemaError.dataPath || "/"; + addError( + errors, + "RESULT_SCHEMA_INVALID", + errorPath, + `${schemaError.keyword}: ${schemaError.message}`, + ); + if (errors.length >= MAXIMUM_DIAGNOSTICS) break; + } + return finishStructure(errors); + } + + appendNestedDiagnostics( + errors, + validateComponentRef(document.executor).errors, + "/executor", + ); + if (document.subject.kind === "component") { + appendNestedDiagnostics( + errors, + validateComponentRef(document.subject.component).errors, + "/subject/component", + ); + } + + const capability = capabilityByName.get(document.invocation.operation); + if (!capability) { + addError( + errors, + "RESULT_OPERATION_UNKNOWN", + "/invocation/operation", + `operation ${document.invocation.operation} is not registered`, + ); + } else { + if (capability.role !== document.executor.role) { + addError( + errors, + "RESULT_OPERATION_ROLE_MISMATCH", + "/invocation/operation", + `operation ${capability.name} belongs to role ${capability.role}`, + ); + } + if ((capability.family ?? null) !== (document.executor.family ?? null)) { + addError( + errors, + "RESULT_OPERATION_FAMILY_MISMATCH", + "/invocation/operation", + `operation ${capability.name} does not match the executor family`, + ); + } + } + + if (document.invocation.harness.name !== document.executor.harness) { + addError( + errors, + "RESULT_EXECUTOR_HARNESS_MISMATCH", + "/invocation/harness/name", + "invocation harness must match the executor harness", + ); + } + validateHarnessVersion(document.invocation.harness.version, document.outcome, errors); + validateScopeHarness( + document.invocation.scope.kind, + document.invocation.harness.name, + errors, + ); + + if ( + document.subject.kind === "component" + && document.subject.component.harness !== document.invocation.harness.name + ) { + addError( + errors, + "RESULT_SUBJECT_HARNESS_MISMATCH", + "/subject/component/harness", + "component subject harness must match the invocation harness", + ); + } + + const reason = reasonByCode.get(document.reason_code); + if (!reason) { + addError( + errors, + "RESULT_REASON_UNKNOWN", + "/reason_code", + `reason code ${document.reason_code} is not registered`, + ); + } else if (reason.outcome !== document.outcome) { + addError( + errors, + "RESULT_REASON_OUTCOME_MISMATCH", + "/reason_code", + `reason ${reason.code} is registered for outcome ${reason.outcome}`, + ); + } + + validateOperationSemantics(document, errors); + validateEffectSemantics(document, errors); + return finishStructure(errors); +} + +export function validateUnboundResultEnvelopeBytes(resultBytes) { + const parsed = parseJsonObjectBytes(resultBytes, "RESULT", "/"); + if (!parsed.valid) return finishStructure(parsed.errors); + return validateUnboundResultEnvelope(parsed.value); +} + +export function validateBoundResultEnvelope({ + resultBytes, + metadataBytesByDigest, + expectedContext, +} = {}) { + const parsedResult = parseJsonObjectBytes(resultBytes, "RESULT", "/"); + if (!parsedResult.valid) return finishBound(parsedResult.errors, []); + + const document = parsedResult.value; + const structureResult = validateUnboundResultEnvelope(document); + const errors = [...structureResult.errors]; + const warnings = []; + if (!structureResult.valid || validateResultSchema === null) { + return finishBound(errors, warnings); + } + + if ( + !isPlainObject(expectedContext) + || !isDeepStrictEqual( + Object.keys(expectedContext).sort(), + ["executor", "invocation", "subject"], + ) + ) { + addError( + errors, + "RESULT_EXPECTED_CONTEXT_REQUIRED", + "/", + "bound validation requires expected executor, subject, and invocation", + ); + } else { + if (!isDeepStrictEqual(document.executor, expectedContext.executor)) { + addError( + errors, + "RESULT_EXECUTOR_CONTEXT_MISMATCH", + "/executor", + "result executor does not match the caller's expected executor", + ); + } + if (!isDeepStrictEqual(document.subject, expectedContext.subject)) { + addError( + errors, + "RESULT_SUBJECT_CONTEXT_MISMATCH", + "/subject", + "result subject does not match the caller's expected subject", + ); + } + if (!isDeepStrictEqual(document.invocation, expectedContext.invocation)) { + addError( + errors, + "RESULT_INVOCATION_MISMATCH", + "/invocation", + "result invocation does not match the caller's expected invocation", + ); + } + } + + const executorBinding = resolveAndValidateMetadata({ + componentRef: document.executor, + metadataBytesByDigest, + label: "EXECUTOR", + errorPath: "/executor", + errors, + warnings, + }); + + let subjectBinding = null; + if (document.subject.kind === "component") { + subjectBinding = resolveAndValidateMetadata({ + componentRef: document.subject.component, + metadataBytesByDigest, + label: "SUBJECT", + errorPath: "/subject/component", + errors, + warnings, + }); + } + + if (executorBinding) { + const operation = document.invocation.operation; + if (!executorBinding.skill.clawsec.provides.includes(operation)) { + addError( + errors, + "RESULT_OPERATION_NOT_DECLARED", + "/invocation/operation", + `executor metadata does not declare operation ${operation}`, + ); + } + + const harnessVersion = document.invocation.harness.version; + if ( + harnessVersion !== null + && isStrictSemver(harnessVersion) + && !versionIsInRange(harnessVersion, executorBinding.skill.clawsec.supported_harness) + && !SAFE_REFUSAL_OUTCOMES.has(document.outcome) + ) { + addError( + errors, + "RESULT_EXECUTOR_VERSION_UNSUPPORTED", + "/invocation/harness/version", + "harness version is outside the executor's declared support range", + ); + } + } + + if (subjectBinding) { + const harnessVersion = document.invocation.harness.version; + if ( + harnessVersion !== null + && isStrictSemver(harnessVersion) + && !versionIsInRange(harnessVersion, subjectBinding.skill.clawsec.supported_harness) + && !SAFE_REFUSAL_OUTCOMES.has(document.outcome) + ) { + addError( + errors, + "RESULT_SUBJECT_VERSION_UNSUPPORTED", + "/invocation/harness/version", + "harness version is outside the component subject's declared support range", + ); + } + } + + validateBoundHarnessSemantics(document, subjectBinding?.skill ?? null, errors); + return finishBound(errors, warnings); +} + +export function digestBytes(bytes) { + if (!(bytes instanceof Uint8Array)) { + throw new TypeError("digestBytes requires Buffer or Uint8Array input"); + } + const buffer = Buffer.from(bytes); + return `sha256:${createHash("sha256").update(buffer).digest("hex")}`; +} + +export function exitCodeForOutcome(outcome) { + if (!validateResultEnvelopeRegistries().valid) return null; + return exitCodeByOutcome.get(outcome) ?? null; +} + +function validateCapabilityRegistry(errors) { + const registryPath = "/contracts/capability-registry.json/capabilities"; + if (!Array.isArray(capabilityRegistry.capabilities)) { + addError( + errors, + "RESULT_CAPABILITY_REGISTRY_INVALID", + registryPath, + "capabilities must be an array", + ); + return; + } + + const names = []; + for (let index = 0; index < capabilityRegistry.capabilities.length; index += 1) { + const entry = capabilityRegistry.capabilities[index]; + const entryPath = `${registryPath}/${index}`; + const expectedKeys = entry?.role === "guardian" + ? ["family", "name", "required_for_stable", "role"] + : ["name", "required_for_stable", "role"]; + validateClosedObject(entry, expectedKeys, entryPath, errors); + if ( + !isPlainObject(entry) + || typeof entry.name !== "string" + || !/^(?:core|suite|guardian|posture)\.[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/ + .test(entry.name) + || !["core", "guardian", "suite"].includes(entry.role) + || typeof entry.required_for_stable !== "boolean" + ) { + addError( + errors, + "RESULT_CAPABILITY_ENTRY_INVALID", + entryPath, + "each capability requires canonical name, role, and stability requirement", + ); + continue; + } + names.push(entry.name); + if (entry.role === "guardian" && entry.family !== "drift") { + addError( + errors, + "RESULT_CAPABILITY_FAMILY_INVALID", + `${entryPath}/family`, + "guardian capabilities require drift family in contract v1", + ); + } else if (entry.role !== "guardian" && entry.family !== undefined) { + addError( + errors, + "RESULT_CAPABILITY_FAMILY_FORBIDDEN", + `${entryPath}/family`, + "only guardian capabilities declare a family", + ); + } + } + validateSortedUniqueStrings( + names, + registryPath, + "RESULT_CAPABILITY", + errors, + ); +} + +function validateOutcomeRegistry(errors) { + const registryPath = "/contracts/result-outcomes.json/outcomes"; + if (!Array.isArray(outcomeRegistry.outcomes)) { + addError( + errors, + "RESULT_OUTCOME_REGISTRY_INVALID", + registryPath, + "outcomes must be an array", + ); + return new Set(); + } + + const names = []; + for (let index = 0; index < outcomeRegistry.outcomes.length; index += 1) { + const entry = outcomeRegistry.outcomes[index]; + const entryPath = `${registryPath}/${index}`; + validateClosedObject(entry, ["description", "name"], entryPath, errors); + if ( + !isPlainObject(entry) + || typeof entry.name !== "string" + || typeof entry.description !== "string" + || entry.description.length === 0 + ) { + addError( + errors, + "RESULT_OUTCOME_ENTRY_INVALID", + entryPath, + "each outcome requires non-empty name and description strings", + ); + continue; + } + names.push(entry.name); + } + + validateSortedUniqueStrings(names, registryPath, "RESULT_OUTCOME", errors); + if (!sameStringSet(names, EXPECTED_OUTCOMES)) { + addError( + errors, + "RESULT_OUTCOME_SET_MISMATCH", + registryPath, + "outcomes must contain exactly the v1 outcome vocabulary", + ); + } + return new Set(names); +} + +function validateReasonRegistry(outcomes, errors) { + const registryPath = "/contracts/result-reason-codes.json/reasons"; + if (!Array.isArray(reasonRegistry.reasons)) { + addError( + errors, + "RESULT_REASON_REGISTRY_INVALID", + registryPath, + "reasons must be an array", + ); + return; + } + + const codes = []; + const coveredOutcomes = new Set(); + for (let index = 0; index < reasonRegistry.reasons.length; index += 1) { + const entry = reasonRegistry.reasons[index]; + const entryPath = `${registryPath}/${index}`; + validateClosedObject(entry, ["code", "description", "outcome"], entryPath, errors); + if ( + !isPlainObject(entry) + || typeof entry.code !== "string" + || !/^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$/.test(entry.code) + || typeof entry.description !== "string" + || entry.description.length === 0 + || typeof entry.outcome !== "string" + ) { + addError( + errors, + "RESULT_REASON_ENTRY_INVALID", + entryPath, + "each reason requires a canonical code, outcome, and description", + ); + continue; + } + codes.push(entry.code); + if (!outcomes.has(entry.outcome)) { + addError( + errors, + "RESULT_REASON_OUTCOME_UNKNOWN", + `${entryPath}/outcome`, + `reason ${entry.code} references unknown outcome ${entry.outcome}`, + ); + } else { + coveredOutcomes.add(entry.outcome); + } + } + + validateSortedUniqueStrings(codes, registryPath, "RESULT_REASON", errors); + for (const outcome of EXPECTED_OUTCOMES) { + if (!coveredOutcomes.has(outcome)) { + addError( + errors, + "RESULT_REASON_OUTCOME_UNCOVERED", + registryPath, + `outcome ${outcome} has no registered reason code`, + ); + } + } +} + +function validateExitCodeRegistry(outcomes, errors) { + const registryPath = "/contracts/exit-codes.json"; + if (exitCodeRegistry.passthrough_child_exit_codes !== false) { + addError( + errors, + "RESULT_EXIT_PASSTHROUGH_FORBIDDEN", + `${registryPath}/passthrough_child_exit_codes`, + "child exit codes must not pass through", + ); + } + if (!Array.isArray(exitCodeRegistry.entries)) { + addError( + errors, + "RESULT_EXIT_REGISTRY_INVALID", + `${registryPath}/entries`, + "entries must be an array", + ); + return; + } + + const seenCodes = new Set(); + const seenOutcomes = new Set(); + let previousCode = -1; + for (let index = 0; index < exitCodeRegistry.entries.length; index += 1) { + const entry = exitCodeRegistry.entries[index]; + const entryPath = `${registryPath}/entries/${index}`; + validateClosedObject(entry, ["code", "outcome"], entryPath, errors); + if ( + !isPlainObject(entry) + || !Number.isInteger(entry.code) + || entry.code < 0 + || entry.code > 255 + || typeof entry.outcome !== "string" + ) { + addError( + errors, + "RESULT_EXIT_ENTRY_INVALID", + entryPath, + "each exit entry requires a unique byte-sized code and outcome", + ); + continue; + } + if (entry.code <= previousCode) { + addError( + errors, + "RESULT_EXIT_CODES_NOT_SORTED", + entryPath, + "exit entries must be sorted by numeric code", + ); + } + previousCode = entry.code; + if (seenCodes.has(entry.code)) { + addError( + errors, + "RESULT_EXIT_CODE_DUPLICATE", + `${entryPath}/code`, + `exit code ${entry.code} appears more than once`, + ); + } + if (seenOutcomes.has(entry.outcome)) { + addError( + errors, + "RESULT_EXIT_OUTCOME_DUPLICATE", + `${entryPath}/outcome`, + `outcome ${entry.outcome} appears more than once`, + ); + } + seenCodes.add(entry.code); + seenOutcomes.add(entry.outcome); + if (!outcomes.has(entry.outcome)) { + addError( + errors, + "RESULT_EXIT_OUTCOME_UNKNOWN", + `${entryPath}/outcome`, + `exit entry references unknown outcome ${entry.outcome}`, + ); + } + if (EXPECTED_EXIT_CODES[entry.outcome] !== entry.code) { + addError( + errors, + "RESULT_EXIT_MAPPING_MISMATCH", + entryPath, + `outcome ${entry.outcome} must map to exit code ${EXPECTED_EXIT_CODES[entry.outcome]}`, + ); + } + } + + if (!sameStringSet([...seenOutcomes], EXPECTED_OUTCOMES)) { + addError( + errors, + "RESULT_EXIT_OUTCOME_SET_MISMATCH", + `${registryPath}/entries`, + "every result outcome must have exactly one exit-code mapping", + ); + } +} + +function validateOperationPolicyRegistry(outcomes, errors) { + const registryPath = "/contracts/result-operation-policy.json"; + if (!isPlainObject(operationPolicy.operations)) { + addError( + errors, + "RESULT_OPERATION_POLICY_INVALID", + `${registryPath}/operations`, + "operations must be an object", + ); + return; + } + if (!isPlainObject(operationPolicy.effect_classes)) { + addError( + errors, + "RESULT_EFFECT_CLASS_POLICY_INVALID", + `${registryPath}/effect_classes`, + "effect_classes must be an object", + ); + return; + } + + const operationNames = Object.keys(operationPolicy.operations); + const capabilityNames = [...capabilityByName.keys()]; + validateSortedUniqueStrings( + operationNames, + `${registryPath}/operations`, + "RESULT_OPERATION_POLICY", + errors, + ); + if (!sameStringSet(operationNames, capabilityNames)) { + addError( + errors, + "RESULT_OPERATION_POLICY_SET_MISMATCH", + `${registryPath}/operations`, + "operation policy keys must exactly match the capability registry", + ); + } + + const effectClassNames = Object.keys(operationPolicy.effect_classes); + const expectedEffectClasses = ["mutation", "planning", "read_only"]; + if (!isDeepStrictEqual(effectClassNames, expectedEffectClasses)) { + addError( + errors, + "RESULT_EFFECT_CLASS_SET_MISMATCH", + `${registryPath}/effect_classes`, + "effect classes must be mutation, planning, and read_only in lexical order", + ); + } + + for (const operationName of operationNames) { + const entry = operationPolicy.operations[operationName]; + const entryPath = `${registryPath}/operations/${escapeJsonPointerToken(operationName)}`; + validateClosedObject(entry, ["effect_class", "subject_kinds"], entryPath, errors); + if ( + !isPlainObject(entry) + || !expectedEffectClasses.includes(entry.effect_class) + || !Array.isArray(entry.subject_kinds) + || entry.subject_kinds.length === 0 + ) { + addError( + errors, + "RESULT_OPERATION_POLICY_ENTRY_INVALID", + entryPath, + "each operation requires an effect class and at least one subject kind", + ); + continue; + } + validateSortedUniqueStrings( + entry.subject_kinds, + `${entryPath}/subject_kinds`, + "RESULT_SUBJECT_KIND", + errors, + ); + for (const subjectKind of entry.subject_kinds) { + if (!["component", "executor", "scope"].includes(subjectKind)) { + addError( + errors, + "RESULT_SUBJECT_KIND_UNKNOWN", + `${entryPath}/subject_kinds`, + `unknown subject kind ${subjectKind}`, + ); + } + } + } + + for (const effectClass of expectedEffectClasses) { + const classPath = `${registryPath}/effect_classes/${effectClass}`; + const rules = operationPolicy.effect_classes[effectClass]; + if (!isPlainObject(rules)) { + addError( + errors, + "RESULT_EFFECT_CLASS_RULES_INVALID", + classPath, + "effect class rules must be an object", + ); + continue; + } + const ruleOutcomes = Object.keys(rules); + if ( + !sameStringSet(ruleOutcomes, [...outcomes]) + || !isDeepStrictEqual(ruleOutcomes, [...ruleOutcomes].sort()) + ) { + addError( + errors, + "RESULT_EFFECT_CLASS_OUTCOMES_MISMATCH", + classPath, + "each effect class must define every outcome in lexical order", + ); + } + for (const outcome of ruleOutcomes) { + const rule = rules[outcome]; + const rulePath = `${classPath}/${outcome}`; + if (rule === null) continue; + validateClosedObject(rule, ["allowed_states", "minimum_count"], rulePath, errors); + if ( + !isPlainObject(rule) + || !Array.isArray(rule.allowed_states) + || !Number.isInteger(rule.minimum_count) + || rule.minimum_count < 0 + || rule.minimum_count > 1 + ) { + addError( + errors, + "RESULT_EFFECT_RULE_INVALID", + rulePath, + "effect rule requires allowed_states and minimum_count of zero or one", + ); + continue; + } + validateSortedUniqueStrings( + rule.allowed_states, + `${rulePath}/allowed_states`, + "RESULT_EFFECT_STATE", + errors, + ); + for (const state of rule.allowed_states) { + if (!["applied", "failed", "proposed", "rolled_back"].includes(state)) { + addError( + errors, + "RESULT_EFFECT_STATE_UNKNOWN", + `${rulePath}/allowed_states`, + `unknown effect state ${state}`, + ); + } + } + if (rule.minimum_count > 0 && rule.allowed_states.length === 0) { + addError( + errors, + "RESULT_EFFECT_RULE_UNSATISFIABLE", + rulePath, + "a positive minimum_count requires at least one allowed state", + ); + } + } + } + + validateRequiredEffectInvariants(errors); +} + +function validateRequiredEffectInvariants(errors) { + const policies = operationPolicy.effect_classes; + const expected = { + mutation: { + blocked: [["proposed"], 0], + confirmation_required: [["proposed"], 1], + degraded: null, + error: [["applied", "failed", "proposed", "rolled_back"], 0], + finding: null, + not_applicable: [[], 0], + pass: [["applied"], 1], + unsupported: [[], 0], + }, + planning: { + blocked: [["proposed"], 0], + confirmation_required: null, + degraded: [["proposed"], 0], + error: [[], 0], + finding: null, + not_applicable: [[], 0], + pass: [["proposed"], 1], + unsupported: [[], 0], + }, + read_only: { + blocked: [[], 0], + confirmation_required: null, + degraded: [[], 0], + error: [[], 0], + finding: [[], 0], + not_applicable: [[], 0], + pass: [[], 0], + unsupported: [[], 0], + }, + }; + + for (const [effectClass, outcomes] of Object.entries(expected)) { + for (const [outcome, expectedRule] of Object.entries(outcomes)) { + const actualRule = policies?.[effectClass]?.[outcome]; + const normalizedActual = actualRule === null + ? null + : [actualRule?.allowed_states, actualRule?.minimum_count]; + if (!isDeepStrictEqual(normalizedActual, expectedRule)) { + addError( + errors, + "RESULT_EFFECT_INVARIANT_MISMATCH", + `/contracts/result-operation-policy.json/effect_classes/${effectClass}/${outcome}`, + "effect rule does not match the required v1 safety invariant", + ); + } + } + } +} + +function validateRegistryHeader(document, expectedContract, errorPath, errors) { + if (!isPlainObject(document)) { + addError( + errors, + "RESULT_REGISTRY_INVALID", + errorPath, + "registry must contain a JSON object", + ); + return; + } + if (document.contract !== expectedContract) { + addError( + errors, + "RESULT_REGISTRY_CONTRACT_MISMATCH", + `${errorPath}/contract`, + `expected contract ${expectedContract}`, + ); + } + if (document.contract_version !== "1") { + addError( + errors, + "RESULT_REGISTRY_VERSION_MISMATCH", + `${errorPath}/contract_version`, + "expected contract version 1", + ); + } +} + +function validateHarnessVersion(version, outcome, errors) { + if (version === null) { + if (!NULL_VERSION_OUTCOMES.has(outcome)) { + addError( + errors, + "RESULT_HARNESS_VERSION_REQUIRED", + "/invocation/harness/version", + "a harness version may be null only for blocked or error outcomes", + ); + } + return; + } + try { + parseSemverV2(version); + } catch { + addError( + errors, + "RESULT_HARNESS_VERSION_INVALID", + "/invocation/harness/version", + "harness version must be strict SemVer", + ); + } +} + +function validateScopeHarness(scopeKind, harness, errors) { + if (scopeKind === "host") return; + if (!scopeKind.startsWith(`${harness}.`)) { + addError( + errors, + "RESULT_SCOPE_HARNESS_MISMATCH", + "/invocation/scope/kind", + `scope ${scopeKind} does not belong to harness ${harness}`, + ); + } +} + +function validateOperationSemantics(document, errors) { + const policy = operationPolicy.operations?.[document.invocation.operation]; + if (!isPlainObject(policy)) return; + + if (!policy.subject_kinds.includes(document.subject.kind)) { + addError( + errors, + "RESULT_SUBJECT_KIND_FORBIDDEN", + "/subject/kind", + `operation ${document.invocation.operation} does not accept subject kind ` + + document.subject.kind, + ); + } + if ( + document.invocation.operation === "core.verify-release" + && document.subject.kind === "scope" + && !SAFE_REFUSAL_OUTCOMES.has(document.outcome) + ) { + addError( + errors, + "RESULT_RELEASE_COMPONENT_SUBJECT_REQUIRED", + "/subject", + "successful release verification requires an exact component subject", + ); + } + + const rule = operationPolicy.effect_classes?.[policy.effect_class]?.[document.outcome]; + if (rule === null || rule === undefined) { + addError( + errors, + "RESULT_OUTCOME_FORBIDDEN", + "/outcome", + `outcome ${document.outcome} is invalid for ${policy.effect_class} operation ` + + document.invocation.operation, + ); + return; + } + + if (document.effects.length < rule.minimum_count) { + addError( + errors, + "RESULT_EFFECT_COUNT_INSUFFICIENT", + "/effects", + `outcome ${document.outcome} requires at least ${rule.minimum_count} effect`, + ); + } + for (let index = 0; index < document.effects.length; index += 1) { + const effect = document.effects[index]; + if (!rule.allowed_states.includes(effect.state)) { + addError( + errors, + "RESULT_EFFECT_STATE_FORBIDDEN", + `/effects/${index}/state`, + `state ${effect.state} is invalid for ${policy.effect_class} outcome ` + + document.outcome, + ); + } + } +} + +function validateBoundHarnessSemantics(document, subjectSkill, errors) { + if (document.executor.harness !== "nanoclaw") return; + const operation = document.invocation.operation; + if ( + NANOCLAW_CHECKOUT_OPERATIONS.has(operation) + && document.invocation.scope.kind !== "nanoclaw.checkout" + ) { + addError( + errors, + "RESULT_NANOCLAW_CHECKOUT_SCOPE_REQUIRED", + "/invocation/scope/kind", + `NanoClaw operation ${operation} requires an explicit checkout scope`, + ); + } + + if ( + !subjectSkill + || !NANOCLAW_PACKAGE_TREE_OPERATIONS.has(operation) + || !["confirmation_required", "pass"].includes(document.outcome) + ) { + return; + } + const installLocation = subjectSkill.clawsec.native?.install_location; + const identifiesPackageTree = document.effects.some((effect) => ( + effect.target.kind === "relative_path" + && effect.target.value === installLocation + )); + if (!identifiesPackageTree) { + addError( + errors, + "RESULT_NANOCLAW_PACKAGE_TREE_EFFECT_REQUIRED", + "/effects", + "NanoClaw package operation must identify the subject's declared install location", + ); + } +} + +function validateEffectSemantics(document, errors) { + const effectIds = []; + for (let index = 0; index < document.effects.length; index += 1) { + const effect = document.effects[index]; + effectIds.push(effect.effect_id); + if ( + effect.target.kind === "relative_path" + && !isNormalizedRelativePath(effect.target.value) + ) { + addError( + errors, + "RESULT_EFFECT_PATH_INVALID", + `/effects/${index}/target/value`, + "effect target must be a normalized portable relative path", + ); + } + } + validateSortedUniqueStrings( + effectIds, + "/effects", + "RESULT_EFFECT_ID", + errors, + ); +} + +function resolveAndValidateMetadata({ + componentRef, + metadataBytesByDigest, + label, + errorPath, + errors, + warnings, +}) { + const suppliedBytes = resolveDigestBytes( + metadataBytesByDigest, + componentRef.metadata_digest, + ); + if (!(suppliedBytes instanceof Uint8Array)) { + addError( + errors, + `RESULT_${label}_METADATA_MISSING`, + `${errorPath}/metadata_digest`, + `no metadata bytes were supplied for ${componentRef.metadata_digest}`, + ); + return null; + } + const bytes = Buffer.from(suppliedBytes); + + const actualDigest = digestBytes(bytes); + if (actualDigest !== componentRef.metadata_digest) { + addError( + errors, + `RESULT_${label}_METADATA_DIGEST_MISMATCH`, + `${errorPath}/metadata_digest`, + "component reference does not match the exact supplied metadata bytes", + ); + return null; + } + + const parsed = parseJsonObjectBytes( + bytes, + `${label}_METADATA`, + `${errorPath}/metadata`, + ); + if (!parsed.valid) { + for (const error of parsed.errors) addError(errors, error.code, error.path, error.message); + return null; + } + + const metadataResult = validateClawsecMetadata({ + skill: parsed.value, + skillDir: null, + requireClawsec: true, + }); + appendNestedDiagnostics( + errors, + metadataResult.errors, + `${errorPath}/metadata`, + `RESULT_${label}_`, + ); + appendNestedDiagnostics( + warnings, + metadataResult.warnings, + `${errorPath}/metadata`, + `RESULT_${label}_`, + ); + if (!metadataResult.valid) return null; + + const expected = componentIdentityFromSkill(parsed.value, actualDigest); + for (const field of ["name", "version", "harness", "role", "family"]) { + if ((componentRef[field] ?? null) !== (expected[field] ?? null)) { + addError( + errors, + `RESULT_${label}_IDENTITY_MISMATCH`, + `${errorPath}/${field}`, + `component reference ${field} does not match exact metadata`, + ); + } + } + + return { + skill: parsed.value, + digest: actualDigest, + }; +} + +function componentIdentityFromSkill(skill, metadataDigest) { + return { + name: skill.name, + version: skill.version, + harness: skill.clawsec.supported_harness.name, + role: skill.clawsec.role, + family: skill.clawsec.family, + metadata_digest: metadataDigest, + }; +} + +function resolveDigestBytes(resolver, digest) { + if (resolver instanceof Map) return resolver.get(digest); + if (isPlainObject(resolver) && Object.hasOwn(resolver, digest)) { + return resolver[digest]; + } + return null; +} + +function versionIsInRange(version, range) { + try { + return compareSemverV2(version, range.minimum_version) >= 0 + && compareSemverV2(version, range.maximum_version_exclusive) < 0; + } catch { + return false; + } +} + +function isStrictSemver(version) { + try { + parseSemverV2(version); + return true; + } catch { + return false; + } +} + +function parseJsonObjectBytes(bytes, label, errorPath) { + const errors = []; + if (!(bytes instanceof Uint8Array)) { + addError( + errors, + `${label}_BYTES_REQUIRED`, + errorPath, + "input must be Buffer or Uint8Array", + ); + return { valid: false, value: null, errors: finish(errors).errors }; + } + const stableBytes = Buffer.from(bytes); + if (stableBytes.length === 0) { + addError(errors, `${label}_EMPTY`, errorPath, "input bytes cannot be empty"); + return { valid: false, value: null, errors: finish(errors).errors }; + } + if (stableBytes.length > RESULT_MAXIMUM_BYTES) { + addError( + errors, + `${label}_TOO_LARGE`, + errorPath, + `input exceeds the ${RESULT_MAXIMUM_BYTES}-byte contract limit`, + ); + return { valid: false, value: null, errors: finish(errors).errors }; + } + if ( + stableBytes[0] === 0xef + && stableBytes[1] === 0xbb + && stableBytes[2] === 0xbf + ) { + addError(errors, `${label}_BOM_FORBIDDEN`, errorPath, "UTF-8 BOM is forbidden"); + return { valid: false, value: null, errors: finish(errors).errors }; + } + + let text; + try { + text = new TextDecoder("utf-8", { fatal: true }).decode(stableBytes); + } catch { + addError(errors, `${label}_UTF8_INVALID`, errorPath, "input is not valid UTF-8"); + return { valid: false, value: null, errors: finish(errors).errors }; + } + if (exceedsJsonNestingDepth(text, RESULT_MAXIMUM_NESTING_DEPTH)) { + addError( + errors, + `${label}_NESTING_TOO_DEEP`, + errorPath, + `JSON nesting exceeds the ${RESULT_MAXIMUM_NESTING_DEPTH}-level contract limit`, + ); + return { valid: false, value: null, errors: finish(errors).errors }; + } + + let value; + try { + value = JSON.parse(text); + } catch { + addError( + errors, + `${label}_JSON_INVALID`, + errorPath, + "input is not one complete JSON document", + ); + return { valid: false, value: null, errors: finish(errors).errors }; + } + + let duplicate; + try { + duplicate = findDuplicateJsonKey(text); + } catch { + addError( + errors, + `${label}_STRUCTURE_INVALID`, + errorPath, + "JSON structure could not be validated deterministically", + ); + return { valid: false, value: null, errors: finish(errors).errors }; + } + if (duplicate) { + addError( + errors, + `${label}_DUPLICATE_KEY`, + errorPath, + "duplicate object keys are forbidden", + ); + } + if (!isPlainObject(value)) { + addError( + errors, + `${label}_ROOT_INVALID`, + errorPath, + "top-level JSON value must be an object", + ); + } + return { + valid: errors.length === 0, + value, + errors: finish(errors).errors, + }; +} + +function exceedsJsonNestingDepth(text, maximumDepth) { + let depth = 0; + let escaped = false; + let inString = false; + for (const character of text) { + if (inString) { + if (escaped) { + escaped = false; + } else if (character === "\\") { + escaped = true; + } else if (character === "\"") { + inString = false; + } + continue; + } + if (character === "\"") { + inString = true; + } else if (character === "{" || character === "[") { + depth += 1; + if (depth > maximumDepth) return true; + } else if (character === "}" || character === "]") { + depth -= 1; + } + } + return false; +} + +function findDuplicateJsonKey(text) { + let index = 0; + + function skipWhitespace() { + while (index < text.length && /[\t\n\r ]/.test(text[index])) index += 1; + } + + function parseString() { + const start = index; + index += 1; + while (index < text.length) { + if (text[index] === "\\") { + index += 2; + } else if (text[index] === "\"") { + index += 1; + return JSON.parse(text.slice(start, index)); + } else { + index += 1; + } + } + return ""; + } + + function parseValue(pointerParts) { + skipWhitespace(); + if (text[index] === "{") return parseObject(pointerParts); + if (text[index] === "[") return parseArray(pointerParts); + if (text[index] === "\"") { + parseString(); + return null; + } + while ( + index < text.length + && !/[\t\n\r ,\]}]/.test(text[index]) + ) { + index += 1; + } + return null; + } + + function parseObject(pointerParts) { + index += 1; + skipWhitespace(); + if (text[index] === "}") { + index += 1; + return null; + } + + const keys = new Set(); + while (index < text.length) { + skipWhitespace(); + const key = parseString(); + const keyPath = [...pointerParts, key]; + if (keys.has(key)) { + return { + key, + path: jsonPointer(keyPath), + }; + } + keys.add(key); + skipWhitespace(); + index += 1; + const duplicate = parseValue(keyPath); + if (duplicate) return duplicate; + skipWhitespace(); + if (text[index] === "}") { + index += 1; + return null; + } + index += 1; + } + return null; + } + + function parseArray(pointerParts) { + index += 1; + skipWhitespace(); + if (text[index] === "]") { + index += 1; + return null; + } + let itemIndex = 0; + while (index < text.length) { + const duplicate = parseValue([...pointerParts, String(itemIndex)]); + if (duplicate) return duplicate; + itemIndex += 1; + skipWhitespace(); + if (text[index] === "]") { + index += 1; + return null; + } + index += 1; + } + return null; + } + + return parseValue([]); +} + +function validateClosedObject(value, expectedKeys, errorPath, errors) { + if (!isPlainObject(value)) return; + const actualKeys = Object.keys(value).sort(); + const sortedExpected = [...expectedKeys].sort(); + if (!isDeepStrictEqual(actualKeys, sortedExpected)) { + addError( + errors, + "RESULT_REGISTRY_ENTRY_FIELDS_INVALID", + errorPath, + `expected fields ${sortedExpected.join(", ")}`, + ); + } +} + +function validateSortedUniqueStrings(values, errorPath, codePrefix, errors) { + const seen = new Set(); + for (let index = 0; index < values.length; index += 1) { + const value = values[index]; + if (seen.has(value)) { + addError( + errors, + `${codePrefix}_DUPLICATE`, + `${errorPath}/${index}`, + `value ${value} appears more than once`, + ); + } + seen.add(value); + if (index > 0 && compareAscii(values[index - 1], value) >= 0) { + addError( + errors, + `${codePrefix}_NOT_SORTED`, + `${errorPath}/${index}`, + "set-like entries must be lexically sorted", + ); + } + } +} + +function sameStringSet(left, right) { + return isDeepStrictEqual([...new Set(left)].sort(), [...new Set(right)].sort()); +} + +function isNormalizedRelativePath(value) { + if (typeof value !== "string") return false; + return /^(?!.*(?:^|\/)\.\.?(?:\/|$))[A-Za-z0-9._@+-]+(?:\/[A-Za-z0-9._@+-]+)*$/ + .test(value); +} + +function appendNestedDiagnostics(errors, nestedErrors, prefix, codePrefix = "RESULT_") { + for (const nested of nestedErrors ?? []) { + const nestedPath = nested.path === "/" ? "" : nested.path; + addError( + errors, + nested.code.startsWith("RESULT_") ? nested.code : `${codePrefix}${nested.code}`, + `${prefix}${nestedPath}` || "/", + nested.message, + ); + } +} + +function isPlainObject(value) { + return value !== null && typeof value === "object" && !Array.isArray(value); +} + +function joinJsonPointer(base, token) { + const prefix = base === "/" ? "" : base; + return `${prefix}/${escapeJsonPointerToken(token)}`; +} + +function jsonPointer(parts) { + if (parts.length === 0) return "/"; + return `/${parts.map(escapeJsonPointerToken).join("/")}`; +} + +function escapeJsonPointerToken(value) { + return String(value).replaceAll("~", "~0").replaceAll("/", "~1"); +} + +function addError(errors, code, errorPath, message) { + const candidate = { + code: sanitizeDiagnosticText(code), + path: sanitizeDiagnosticText(errorPath || "/"), + message: sanitizeDiagnosticText(message), + }; + let diagnosticKeys = diagnosticKeysByArray.get(errors); + if (!diagnosticKeys) { + diagnosticKeys = new Set(errors.map(diagnosticKey)); + diagnosticKeysByArray.set(errors, diagnosticKeys); + } + const candidateKey = diagnosticKey(candidate); + if (diagnosticKeys.has(candidateKey)) return; + if (errors.length >= MAXIMUM_DIAGNOSTICS - 1) { + const truncated = { + code: "RESULT_DIAGNOSTICS_TRUNCATED", + path: "/", + message: `validation stopped after ${MAXIMUM_DIAGNOSTICS - 1} diagnostics`, + }; + const truncatedKey = diagnosticKey(truncated); + if (!diagnosticKeys.has(truncatedKey)) { + diagnosticKeys.add(truncatedKey); + errors.push(truncated); + } + return; + } + diagnosticKeys.add(candidateKey); + errors.push(candidate); +} + +function diagnosticKey(diagnostic) { + return `${diagnostic.code}\u0000${diagnostic.path}\u0000${diagnostic.message}`; +} + +function sanitizeDiagnosticText(value) { + return Array.from(String(value), (character) => { + const codePoint = character.codePointAt(0); + if (codePoint <= 0x1f || (codePoint >= 0x7f && codePoint <= 0x9f)) { + return `\\u${codePoint.toString(16).padStart(4, "0")}`; + } + return character; + }).join(""); +} + +function finishStructure(errors) { + return finish(errors, "unverified"); +} + +function finishBound(errors, warnings) { + const result = finish(errors, errors.length === 0 ? "exact_metadata" : "failed"); + result.authorization = "unverified"; + result.warnings = [...warnings].sort(compareDiagnostics); + return result; +} + +function finish(errors, binding = null) { + const result = { + valid: errors.length === 0, + errors: [...errors].sort(compareDiagnostics), + }; + if (binding !== null) result.binding = binding; + return result; +} + +function compareDiagnostics(left, right) { + return compareAscii(left.code, right.code) + || compareAscii(left.path, right.path) + || compareAscii(left.message, right.message); +} + +function compareAscii(left, right) { + if (left < right) return -1; + if (left > right) return 1; + return 0; +} + +async function readContractJson(filePath, label, errorPath) { + try { + return parseJsonObjectBytes(await readFile(filePath), label, errorPath); + } catch { + return { + valid: false, + value: null, + errors: [{ + code: `${label}_READ_FAILED`, + path: errorPath, + message: "contract source could not be read", + }], + }; + } +} + +function parseArguments(argv) { + const options = { + resultPath: null, + metadataPaths: [], + expectedContextPath: null, + json: false, + }; + + for (let index = 0; index < argv.length; index += 1) { + const argument = argv[index]; + if (argument === "--result") { + options.resultPath = argv[index + 1] ?? null; + index += 1; + } else if (argument === "--metadata") { + options.metadataPaths.push(argv[index + 1] ?? ""); + index += 1; + } else if (argument === "--expected-context") { + options.expectedContextPath = argv[index + 1] ?? null; + index += 1; + } else if (argument === "--json") { + options.json = true; + } else { + throw new Error(`Unknown argument: ${argument}`); + } + } + + if ( + !options.resultPath + || options.metadataPaths.length === 0 + || !options.expectedContextPath + ) { + throw new Error( + "Usage: node scripts/ci/validate_clawsec_result_envelope.mjs " + + "--result --metadata [--metadata ] " + + "--expected-context [--json]", + ); + } + return options; +} + +async function main() { + let options; + let result; + try { + options = parseArguments(process.argv.slice(2)); + const [ + resultBytes, + expectedContextBytes, + ...metadataByteValues + ] = await Promise.all([ + readFile(path.resolve(options.resultPath)), + readFile(path.resolve(options.expectedContextPath)), + ...options.metadataPaths.map((entry) => readFile(path.resolve(entry))), + ]); + const expected = parseJsonObjectBytes( + expectedContextBytes, + "EXPECTED_CONTEXT", + "/", + ); + if (!expected.valid) { + result = finish(expected.errors); + } else { + const metadataBytesByDigest = new Map( + metadataByteValues.map((bytes) => [digestBytes(bytes), bytes]), + ); + result = validateBoundResultEnvelope({ + resultBytes, + metadataBytesByDigest, + expectedContext: expected.value, + }); + } + } catch { + result = finish([{ + code: "RESULT_VALIDATOR_ERROR", + path: "/", + message: "validator invocation failed; check arguments and readable input files", + }]); + } + + if (options?.json) { + process.stdout.write(`${JSON.stringify(result)}\n`); + } else if (result.valid) { + process.stdout.write( + "CONTRACT_VALID: ClawSec base result v1 with exact metadata bytes; " + + "release authorization and operation proof remain unverified\n", + ); + for (const warning of result.warnings) { + process.stdout.write(`WARNING ${warning.code} ${warning.path}: ${warning.message}\n`); + } + } else { + for (const error of result.errors) { + process.stderr.write(`${error.code} ${error.path}: ${error.message}\n`); + } + } + process.exitCode = result.valid ? 0 : 1; +} + +const isDirectInvocation = process.argv[1] + && pathToFileURL(path.resolve(process.argv[1])).href === import.meta.url; +if (isDirectInvocation) { + await main(); +} diff --git a/scripts/test-skill-result-envelope-contract.mjs b/scripts/test-skill-result-envelope-contract.mjs new file mode 100644 index 00000000..a77e18c5 --- /dev/null +++ b/scripts/test-skill-result-envelope-contract.mjs @@ -0,0 +1,1146 @@ +#!/usr/bin/env node + +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import { createHash } from "node:crypto"; +import { mkdtemp, readdir, readFile, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +import { + digestBytes, + exitCodeForOutcome, + validateBoundResultEnvelope, + validateUnboundResultEnvelope, + validateUnboundResultEnvelopeBytes, + validateResultEnvelopeRegistries, +} from "./ci/validate_clawsec_result_envelope.mjs"; + +const repositoryRoot = fileURLToPath(new URL("../", import.meta.url)); +const metadataFixtureRoot = path.join( + repositoryRoot, + "contracts/fixtures/component-metadata-v1/valid", +); +const resultFixtureRoot = path.join( + repositoryRoot, + "contracts/fixtures/result-v1/valid", +); + +const [ + coreNanoclawBytes, + coreOpenclawBytes, + suiteNanoclawBytes, + driftNanoclawBytes, + driftPicoclawBytes, + suiteHermesBytes, + doctorErrorBytes, + doctorPassBytes, +] = await Promise.all([ + readFile(path.join(metadataFixtureRoot, "core-nanoclaw-v2.json")), + readFile(path.join(metadataFixtureRoot, "core-openclaw.json")), + readFile(path.join(metadataFixtureRoot, "suite-nanoclaw-v2.json")), + readFile(path.join(metadataFixtureRoot, "drift-nanoclaw-v2.json")), + readFile(path.join(metadataFixtureRoot, "drift-picoclaw.json")), + readFile(path.join(metadataFixtureRoot, "suite-hermes.json")), + readFile(path.join( + resultFixtureRoot, + "core-doctor-error-unknown-version-nanoclaw-v2.json", + )), + readFile(path.join( + resultFixtureRoot, + "core-doctor-pass-nanoclaw-v2.json", + )), +]); + +const coreNanoclawRef = componentRefFromMetadata(coreNanoclawBytes); +const coreOpenclawRef = componentRefFromMetadata(coreOpenclawBytes); +const suiteNanoclawRef = componentRefFromMetadata(suiteNanoclawBytes); +const driftNanoclawRef = componentRefFromMetadata(driftNanoclawBytes); +const driftPicoclawRef = componentRefFromMetadata(driftPicoclawBytes); +const suiteHermesRef = componentRefFromMetadata(suiteHermesBytes); +const doctorPass = JSON.parse(doctorPassBytes); + +assertValid(validateResultEnvelopeRegistries(), "result registries"); +assert.equal( + digestBytes(coreNanoclawBytes), + `sha256:${createHash("sha256").update(coreNanoclawBytes).digest("hex")}`, + "digestBytes must hash exact bytes", +); + +for (const [bytes, exitCode] of [ + [doctorErrorBytes, 1], + [doctorPassBytes, 0], +]) { + const document = JSON.parse(bytes); + const result = validateBoundResultEnvelope({ + resultBytes: bytes, + metadataBytesByDigest: metadataMap(coreNanoclawBytes), + expectedContext: expectedContextFor(document), + }); + assertValid(result, `committed fixture ${document.invocation.id}`); + assert.equal(result.binding, "exact_metadata"); + assert.equal(exitCodeForOutcome(document.outcome), exitCode); +} + +const cliDirectory = await mkdtemp(path.join(tmpdir(), "clawsec-result-cli-")); +const cliContextPath = path.join(cliDirectory, "context.json"); +await writeFile( + cliContextPath, + `${JSON.stringify(expectedContextFor(doctorPass))}\n`, + "utf8", +); +const cliResult = spawnSync(process.execPath, [ + path.join(repositoryRoot, "scripts/ci/validate_clawsec_result_envelope.mjs"), + "--result", + path.join(resultFixtureRoot, "core-doctor-pass-nanoclaw-v2.json"), + "--metadata", + path.join(metadataFixtureRoot, "core-nanoclaw-v2.json"), + "--expected-context", + cliContextPath, + "--json", +], { + cwd: repositoryRoot, + encoding: "utf8", +}); +assert.equal(cliResult.status, 0, `CLI failed: ${cliResult.stderr}`); +assert.equal(cliResult.stderr, ""); +assert.deepEqual(JSON.parse(cliResult.stdout), { + valid: true, + errors: [], + binding: "exact_metadata", + authorization: "unverified", + warnings: [], +}); + +const unboundPass = validateUnboundResultEnvelopeBytes(doctorPassBytes); +assertValid(unboundPass, "unbound fixture"); +assert.equal( + unboundPass.binding, + "unverified", + "unbound validation must disclose that metadata is not bound", +); + +const unresolvedDigest = clone(doctorPass); +unresolvedDigest.executor.metadata_digest = `sha256:${"0".repeat(64)}`; +const unresolvedUnbound = validateUnboundResultEnvelopeBytes(encode(unresolvedDigest)); +assertValid(unresolvedUnbound, "unbound valid unresolved digest"); +assert.equal(unresolvedUnbound.binding, "unverified"); +assertCode( + validateBoundResultEnvelope({ + resultBytes: encode(unresolvedDigest), + metadataBytesByDigest: metadataMap(coreNanoclawBytes), + expectedContext: expectedContextFor(unresolvedDigest), + }), + "RESULT_EXECUTOR_METADATA_MISSING", + "unresolved digest under bound validation", +); + +const stableMetadata = JSON.parse(coreNanoclawBytes); +stableMetadata.clawsec.maturity = "stable"; +const stableMetadataBytes = encode(stableMetadata); +const stableResult = clone(doctorPass); +stableResult.executor = componentRefFromMetadata(stableMetadataBytes); +const stableBoundResult = validateBound(stableResult, [stableMetadataBytes]); +assertValid(stableBoundResult, "stable metadata warning propagation"); +assert( + stableBoundResult.warnings.some((entry) => ( + entry.code === "RESULT_EXECUTOR_STABLE_MATURITY_REQUIRES_CONFORMANCE" + )), + `stable metadata warning was dropped: ${JSON.stringify(stableBoundResult)}`, +); +assert.equal(stableBoundResult.authorization, "unverified"); + +const expectedExitCodes = { + blocked: 3, + confirmation_required: 42, + degraded: 4, + error: 1, + finding: 2, + not_applicable: 6, + pass: 0, + unsupported: 5, +}; +for (const [outcome, exitCode] of Object.entries(expectedExitCodes)) { + assert.equal(exitCodeForOutcome(outcome), exitCode, `exit code for ${outcome}`); +} +assert.equal(exitCodeForOutcome("unknown"), null); + +const suiteStatus = makeResult({ + executor: suiteNanoclawRef, + operation: "suite.status", + subject: { kind: "scope" }, + summary: "Suite status invocation completed; aggregate evidence is not part of this base result.", +}); +assertValid( + validateBound(suiteStatus, [suiteNanoclawBytes]), + "suite role binding", +); + +const guardianStatus = makeResult({ + executor: driftNanoclawRef, + operation: "guardian.status", + subject: { kind: "executor" }, + outcome: "finding", + reasonCode: "security_finding_detected", + summary: "The guardian reports a security-relevant condition.", +}); +assertValid( + validateBound(guardianStatus, [driftNanoclawBytes]), + "guardian role and family binding", +); + +const openclawDoctor = applyFixtureHarness( + makeResult({ + executor: coreOpenclawRef, + operation: "core.doctor", + subject: { kind: "executor" }, + summary: "OpenClaw core doctor invocation completed.", + }), + coreOpenclawBytes, + { + kind: "openclaw.workspace", + ref: "fixture-workspace", + }, +); +assertValid( + validateBound(openclawDoctor, [coreOpenclawBytes]), + "OpenClaw workspace result", +); + +const hermesStatus = applyFixtureHarness( + makeResult({ + executor: suiteHermesRef, + operation: "suite.status", + subject: { kind: "scope" }, + summary: "Hermes suite status invocation completed.", + }), + suiteHermesBytes, + { + kind: "hermes.profile", + ref: "fixture-profile", + }, +); +assertValid( + validateBound(hermesStatus, [suiteHermesBytes]), + "Hermes profile result", +); + +const picoclawGuardianStatus = applyFixtureHarness( + makeResult({ + executor: driftPicoclawRef, + operation: "guardian.status", + subject: { kind: "executor" }, + summary: "PicoClaw drift guardian status invocation completed.", + }), + driftPicoclawBytes, + { + kind: "picoclaw.home", + ref: "fixture-home", + }, +); +assertValid( + validateBound(picoclawGuardianStatus, [driftPicoclawBytes]), + "PicoClaw home result", +); + +const planningPass = makeResult({ + operation: "core.plan-release", + subject: { + kind: "component", + component: suiteNanoclawRef, + }, + summary: "The executor reports a proposed installation plan.", + effects: [effect("plan.suite", "proposed")], +}); +assertValid( + validateBound(planningPass, [coreNanoclawBytes, suiteNanoclawBytes]), + "planning pass with proposed effect", +); + +const mutationPass = makeResult({ + operation: "core.install-release", + subject: { + kind: "component", + component: suiteNanoclawRef, + }, + summary: "The executor reports an applied installation effect; proof requires a receipt.", + effects: [effect("install.suite", "applied")], +}); +assertValid( + validateBound(mutationPass, [coreNanoclawBytes, suiteNanoclawBytes]), + "mutation pass with applied effect", +); + +const nanoclawHostMutation = clone(mutationPass); +nanoclawHostMutation.invocation.scope = { kind: "host" }; +assertCode( + validateBound( + nanoclawHostMutation, + [coreNanoclawBytes, suiteNanoclawBytes], + ), + "RESULT_NANOCLAW_CHECKOUT_SCOPE_REQUIRED", + "NanoClaw host-scope package mutation", +); + +const unrelatedNanoclawEffect = clone(mutationPass); +unrelatedNanoclawEffect.effects[0].target.value = "unrelated/file"; +assertCode( + validateBound( + unrelatedNanoclawEffect, + [coreNanoclawBytes, suiteNanoclawBytes], + ), + "RESULT_NANOCLAW_PACKAGE_TREE_EFFECT_REQUIRED", + "unrelated NanoClaw package effect", +); + +const selfReleaseVerification = makeResult({ + operation: "core.verify-release", + subject: { + kind: "component", + component: coreNanoclawRef, + }, + summary: "The core reports verification of its own component identity.", +}); +assertValid( + validateBound(selfReleaseVerification, [coreNanoclawBytes]), + "self component release verification", +); + +const releasePassWithoutComponent = makeResult({ + operation: "core.verify-release", + subject: { kind: "scope" }, + summary: "The executor reports release verification without a release identity.", +}); +assertCode( + validateBound(releasePassWithoutComponent, [coreNanoclawBytes]), + "RESULT_RELEASE_COMPONENT_SUBJECT_REQUIRED", + "release pass without component identity", +); + +const unsupportedExecutorRange = clone(doctorPass); +unsupportedExecutorRange.invocation.harness.version = "2.2.0"; +unsupportedExecutorRange.outcome = "unsupported"; +unsupportedExecutorRange.reason_code = "intentionally_unsupported"; +unsupportedExecutorRange.summary = + "The core reports that NanoClaw 2.2.0 is outside its declared support range."; +assertValid( + validateBound(unsupportedExecutorRange, [coreNanoclawBytes]), + "safe out-of-range executor refusal", +); + +const narrowSuite = JSON.parse(suiteNanoclawBytes); +narrowSuite.clawsec.supported_harness.maximum_version_exclusive = "2.1.18"; +const narrowSuiteBytes = encode(narrowSuite); +const narrowSuiteRef = componentRefFromMetadata(narrowSuiteBytes); +const blockedSubjectRange = makeResult({ + operation: "core.install-release", + subject: { + kind: "component", + component: narrowSuiteRef, + }, + outcome: "blocked", + reasonCode: "prerequisite_missing", + summary: "The target suite does not support the detected NanoClaw version.", +}); +blockedSubjectRange.invocation.harness.version = "2.1.18"; +assertValid( + validateBound( + blockedSubjectRange, + [coreNanoclawBytes, narrowSuiteBytes], + ), + "safe incompatible-subject refusal", +); + +const outcomeCases = [ + ["blocked", "policy_blocked"], + ["degraded", "assurance_unavailable"], + ["error", "internal_failure"], + ["finding", "security_finding_detected"], + ["not_applicable", "operation_not_applicable"], + ["pass", "operation_completed"], + ["unsupported", "intentionally_unsupported"], +]; +for (const [outcome, reasonCode] of outcomeCases) { + const document = makeResult({ + outcome, + reasonCode, + summary: `Executor reports ${outcome}.`, + }); + assertValid( + validateBound(document, [coreNanoclawBytes]), + `outcome ${outcome}`, + ); +} + +const confirmation = makeResult({ + operation: "core.install-release", + subject: { + kind: "component", + component: suiteNanoclawRef, + }, + outcome: "confirmation_required", + reasonCode: "user_confirmation_required", + summary: "The executor reports that a typed installation plan needs confirmation.", + effects: [effect("install.suite", "proposed")], +}); +assertValid( + validateBound( + confirmation, + [coreNanoclawBytes, suiteNanoclawBytes], + ), + "base confirmation summary", +); + +assertCode( + validateUnboundResultEnvelope({ ...clone(doctorPass), unknown: true }), + "RESULT_SCHEMA_INVALID", + "unknown top-level field", +); + +const badUuid = clone(doctorPass); +badUuid.invocation.id = "not-a-uuid"; +assertCode(validateUnboundResultEnvelope(badUuid), "RESULT_SCHEMA_INVALID", "UUID"); + +const badTimestamp = clone(doctorPass); +badTimestamp.reported_at = "2026-07-23 10:01:00"; +assertCode( + validateUnboundResultEnvelope(badTimestamp), + "RESULT_SCHEMA_INVALID", + "timestamp", +); + +const unknownOperation = clone(doctorPass); +unknownOperation.invocation.operation = "core.unknown-operation"; +assertCode( + validateUnboundResultEnvelope(unknownOperation), + "RESULT_OPERATION_UNKNOWN", + "unknown operation", +); + +const wrongRole = clone(doctorPass); +wrongRole.invocation.operation = "suite.status"; +assertCode( + validateUnboundResultEnvelope(wrongRole), + "RESULT_OPERATION_ROLE_MISMATCH", + "wrong operation role", +); + +const undeclaredOptional = makeResult({ + executor: suiteNanoclawRef, + operation: "suite.recurring-advisory-verification", + subject: { kind: "scope" }, + outcome: "unsupported", + reasonCode: "intentionally_unsupported", + summary: "The optional operation is absent.", +}); +assertCode( + validateBound(undeclaredOptional, [suiteNanoclawBytes]), + "RESULT_OPERATION_NOT_DECLARED", + "undeclared optional capability", +); + +const harnessMismatch = clone(doctorPass); +harnessMismatch.invocation.harness.name = "hermes"; +assertCode( + validateUnboundResultEnvelope(harnessMismatch), + "RESULT_EXECUTOR_HARNESS_MISMATCH", + "executor harness", +); + +const badHarnessVersion = clone(doctorPass); +badHarnessVersion.invocation.harness.version = "2.1"; +const badHarnessVersionResult = validateBound( + badHarnessVersion, + [coreNanoclawBytes], +); +assertCode( + badHarnessVersionResult, + "RESULT_HARNESS_VERSION_INVALID", + "strict harness SemVer", +); +assert( + !badHarnessVersionResult.errors.some((entry) => ( + entry.code === "RESULT_EXECUTOR_VERSION_UNSUPPORTED" + )), + `malformed SemVer produced range cascade: ${JSON.stringify(badHarnessVersionResult.errors)}`, +); + +const missingHarnessVersion = clone(doctorPass); +missingHarnessVersion.invocation.harness.version = null; +assertCode( + validateUnboundResultEnvelope(missingHarnessVersion), + "RESULT_HARNESS_VERSION_REQUIRED", + "null version on pass", +); + +const unsupportedHarnessVersion = clone(doctorPass); +unsupportedHarnessVersion.invocation.harness.version = "2.2.0"; +assertCode( + validateBound(unsupportedHarnessVersion, [coreNanoclawBytes]), + "RESULT_EXECUTOR_VERSION_UNSUPPORTED", + "executor support range", +); + +const scopeMismatch = clone(doctorPass); +scopeMismatch.invocation.scope = { + kind: "hermes.profile", + ref: "default", +}; +assertCode( + validateUnboundResultEnvelope(scopeMismatch), + "RESULT_SCOPE_HARNESS_MISMATCH", + "scope harness", +); + +const hostWithRef = clone(doctorPass); +hostWithRef.invocation.scope = { + kind: "host", + ref: "forbidden", +}; +assertCode( + validateUnboundResultEnvelope(hostWithRef), + "RESULT_SCHEMA_INVALID", + "host scope ref", +); + +const nonHostWithoutRef = clone(doctorPass); +nonHostWithoutRef.invocation.scope = { + kind: "nanoclaw.checkout", +}; +assertCode( + validateUnboundResultEnvelope(nonHostWithoutRef), + "RESULT_SCHEMA_INVALID", + "non-host scope ref", +); + +const unknownReason = clone(doctorPass); +unknownReason.reason_code = "unknown_reason"; +assertCode( + validateUnboundResultEnvelope(unknownReason), + "RESULT_REASON_UNKNOWN", + "unknown reason", +); + +const wrongReasonOutcome = clone(doctorPass); +wrongReasonOutcome.reason_code = "policy_blocked"; +assertCode( + validateUnboundResultEnvelope(wrongReasonOutcome), + "RESULT_REASON_OUTCOME_MISMATCH", + "reason outcome", +); + +const sameComponentSubject = clone(doctorPass); +sameComponentSubject.subject = { + kind: "component", + component: coreNanoclawRef, +}; +assertCode( + validateUnboundResultEnvelope(sameComponentSubject), + "RESULT_SUBJECT_KIND_FORBIDDEN", + "doctor component subject", +); + +const crossHarnessSubject = makeResult({ + operation: "core.install-release", + subject: { + kind: "component", + component: suiteHermesRef, + }, + outcome: "blocked", + reasonCode: "policy_blocked", + summary: "Cross-harness installation is blocked.", +}); +assertCode( + validateBound( + crossHarnessSubject, + [coreNanoclawBytes, suiteHermesBytes], + ), + "RESULT_SUBJECT_HARNESS_MISMATCH", + "cross-harness subject", +); + +const confirmationWithoutEffect = clone(confirmation); +confirmationWithoutEffect.effects = []; +assertCode( + validateUnboundResultEnvelope(confirmationWithoutEffect), + "RESULT_EFFECT_COUNT_INSUFFICIENT", + "confirmation without proposal", +); + +const confirmationWithAppliedEffect = clone(confirmation); +confirmationWithAppliedEffect.effects[0].state = "applied"; +assertCode( + validateUnboundResultEnvelope(confirmationWithAppliedEffect), + "RESULT_EFFECT_STATE_FORBIDDEN", + "confirmation after mutation", +); + +const unsupportedWithEffect = makeResult({ + outcome: "unsupported", + reasonCode: "intentionally_unsupported", + summary: "The declared operation is unsupported in this scope.", + effects: [effect("unexpected.effect", "failed")], +}); +assertCode( + validateUnboundResultEnvelope(unsupportedWithEffect), + "RESULT_EFFECT_STATE_FORBIDDEN", + "unsupported effect", +); + +const duplicateEffects = makeResult({ + outcome: "error", + reasonCode: "internal_failure", + summary: "The executor reports conflicting effect summaries.", + effects: [ + effect("same.id", "failed"), + effect("same.id", "rolled_back"), + ], +}); +assertCode( + validateUnboundResultEnvelope(duplicateEffects), + "RESULT_EFFECT_ID_DUPLICATE", + "duplicate effect IDs", +); + +const unsortedEffects = makeResult({ + outcome: "error", + reasonCode: "internal_failure", + summary: "The executor reports unsorted effect summaries.", + effects: [ + effect("z.effect", "failed"), + effect("a.effect", "rolled_back"), + ], +}); +assertCode( + validateUnboundResultEnvelope(unsortedEffects), + "RESULT_EFFECT_ID_NOT_SORTED", + "effect ordering", +); + +const maximumEffects = makeResult({ + operation: "core.install-release", + subject: { + kind: "component", + component: suiteNanoclawRef, + }, + outcome: "error", + reasonCode: "internal_failure", + summary: "The executor reports the maximum number of bounded effect summaries.", + effects: Array.from( + { length: 64 }, + (_, index) => effect(`bulk.${index.toString().padStart(3, "0")}`, "failed"), + ), +}); +assertValid( + validateUnboundResultEnvelope(maximumEffects), + "maximum effect summary count", +); + +const excessiveEffects = clone(maximumEffects); +excessiveEffects.effects.push(effect("bulk.064", "failed")); +assertCode( + validateUnboundResultEnvelope(excessiveEffects), + "RESULT_SCHEMA_INVALID", + "effect summary maximum", +); + +for (const invalidPath of [ + "/absolute", + "../traversal", + "./dot", + "a/../b", + "a\\b", + "a//b", + "C:/drive", + "https:/example", + "file:secret", + "%2e%2e/secret", + "trailing/", + "non-ascii-\u00e9", +]) { + const document = makeResult({ + outcome: "error", + reasonCode: "internal_failure", + summary: "The executor reports a failed effect.", + effects: [effect("bad.path", "failed", invalidPath)], + }); + const result = validateUnboundResultEnvelope(document); + assert.equal(result.valid, false, `invalid effect path accepted: ${invalidPath}`); + assert( + result.errors.some((entry) => ( + entry.code === "RESULT_SCHEMA_INVALID" + || entry.code === "RESULT_EFFECT_PATH_INVALID" + )), + `missing path diagnostic for ${invalidPath}: ${JSON.stringify(result.errors)}`, + ); +} + +const harnessResourceEffect = makeResult({ + operation: "core.install-release", + subject: { + kind: "component", + component: suiteNanoclawRef, + }, + outcome: "error", + reasonCode: "internal_failure", + summary: "The executor reports a failed harness resource operation.", + effects: [{ + effect_id: "task.series", + state: "failed", + target: { + kind: "harness_resource", + value: "task-series:clawsec-01", + }, + summary: "The harness resource operation failed.", + }], +}); +assertValid( + validateUnboundResultEnvelope(harnessResourceEffect), + "opaque harness resource effect", +); + +const readOnlyAppliedEffect = clone(doctorPass); +readOnlyAppliedEffect.effects = [effect("doctor.write", "applied")]; +assertCode( + validateUnboundResultEnvelope(readOnlyAppliedEffect), + "RESULT_EFFECT_STATE_FORBIDDEN", + "read-only applied effect", +); + +const mutationWithoutAppliedEffect = clone(mutationPass); +mutationWithoutAppliedEffect.effects = []; +assertCode( + validateUnboundResultEnvelope(mutationWithoutAppliedEffect), + "RESULT_EFFECT_COUNT_INSUFFICIENT", + "mutation pass without effect", +); + +const doctorScopeSubject = clone(doctorPass); +doctorScopeSubject.subject = { kind: "scope" }; +assertCode( + validateUnboundResultEnvelope(doctorScopeSubject), + "RESULT_SUBJECT_KIND_FORBIDDEN", + "doctor scope subject", +); + +assertCode( + validateBoundResultEnvelope({ + resultBytes: doctorPassBytes, + metadataBytesByDigest: new Map(), + expectedContext: expectedContextFor(doctorPass), + }), + "RESULT_EXECUTOR_METADATA_MISSING", + "missing executor metadata", +); + +const alteredCoreBytes = Buffer.concat([coreNanoclawBytes, Buffer.from("\n")]); +assertCode( + validateBoundResultEnvelope({ + resultBytes: doctorPassBytes, + metadataBytesByDigest: new Map([ + [coreNanoclawRef.metadata_digest, alteredCoreBytes], + ]), + expectedContext: expectedContextFor(doctorPass), + }), + "RESULT_EXECUTOR_METADATA_DIGEST_MISMATCH", + "exact executor metadata digest", +); + +const executorIdentityMismatch = clone(doctorPass); +executorIdentityMismatch.executor.version = "0.1.0-rc.2"; +assertCode( + validateBound(executorIdentityMismatch, [coreNanoclawBytes]), + "RESULT_EXECUTOR_IDENTITY_MISMATCH", + "executor metadata identity", +); + +const malformedMetadataBytes = Buffer.from("{\"name\":"); +const malformedMetadataResult = clone(doctorPass); +malformedMetadataResult.executor.metadata_digest = digestBytes(malformedMetadataBytes); +assertCode( + validateBoundResultEnvelope({ + resultBytes: encode(malformedMetadataResult), + metadataBytesByDigest: metadataMap(malformedMetadataBytes), + expectedContext: expectedContextFor(malformedMetadataResult), + }), + "EXECUTOR_METADATA_JSON_INVALID", + "malformed executor metadata", +); + +assertCode( + validateBoundResultEnvelope({ + resultBytes: encode(confirmation), + metadataBytesByDigest: metadataMap(coreNanoclawBytes), + expectedContext: expectedContextFor(confirmation), + }), + "RESULT_SUBJECT_METADATA_MISSING", + "missing component subject metadata", +); + +const alteredSuiteBytes = Buffer.concat([suiteNanoclawBytes, Buffer.from("\n")]); +assertCode( + validateBoundResultEnvelope({ + resultBytes: encode(confirmation), + metadataBytesByDigest: new Map([ + [coreNanoclawRef.metadata_digest, coreNanoclawBytes], + [suiteNanoclawRef.metadata_digest, alteredSuiteBytes], + ]), + expectedContext: expectedContextFor(confirmation), + }), + "RESULT_SUBJECT_METADATA_DIGEST_MISMATCH", + "exact subject metadata digest", +); + +assertCode( + validateBoundResultEnvelope({ + resultBytes: doctorPassBytes, + metadataBytesByDigest: metadataMap(coreNanoclawBytes), + }), + "RESULT_EXPECTED_CONTEXT_REQUIRED", + "expected context required", +); + +const wrongExpectedInvocation = clone(doctorPass.invocation); +wrongExpectedInvocation.id = "99999999-9999-4999-8999-999999999999"; +const wrongExpectedContext = expectedContextFor(doctorPass); +wrongExpectedContext.invocation = wrongExpectedInvocation; +assertCode( + validateBoundResultEnvelope({ + resultBytes: doctorPassBytes, + metadataBytesByDigest: metadataMap(coreNanoclawBytes), + expectedContext: wrongExpectedContext, + }), + "RESULT_INVOCATION_MISMATCH", + "expected invocation binding", +); + +const substitutedExecutor = makeResult({ + executor: coreOpenclawRef, + operation: "core.doctor", + subject: { kind: "executor" }, + summary: "An internally consistent but unexpected executor produced this result.", +}); +const coreOpenclawSkill = JSON.parse(coreOpenclawBytes); +substitutedExecutor.invocation.harness.version = + coreOpenclawSkill.clawsec.supported_harness.minimum_version; +substitutedExecutor.invocation.scope = { + kind: "openclaw.workspace", + ref: "fixture-workspace", +}; +assertCode( + validateBound( + substitutedExecutor, + [coreOpenclawBytes], + expectedContextFor(doctorPass), + ), + "RESULT_EXECUTOR_CONTEXT_MISMATCH", + "executor substitution", +); + +const substitutedSubject = clone(confirmation); +substitutedSubject.subject.component = driftNanoclawRef; +substitutedSubject.effects[0].target.value = + JSON.parse(driftNanoclawBytes).clawsec.native.install_location; +assertCode( + validateBound( + substitutedSubject, + [coreNanoclawBytes, driftNanoclawBytes], + expectedContextFor(confirmation), + ), + "RESULT_SUBJECT_CONTEXT_MISMATCH", + "component subject substitution", +); + +assertCode( + validateUnboundResultEnvelopeBytes("not bytes"), + "RESULT_BYTES_REQUIRED", + "byte-only parser", +); +assertCode( + validateUnboundResultEnvelopeBytes( + Buffer.from([0xef, 0xbb, 0xbf, 0x7b, 0x7d]), + ), + "RESULT_BOM_FORBIDDEN", + "BOM", +); +assertCode( + validateUnboundResultEnvelopeBytes(Buffer.from([0xc3, 0x28])), + "RESULT_UTF8_INVALID", + "invalid UTF-8", +); +assertCode( + validateUnboundResultEnvelopeBytes(Buffer.from("{}{}")), + "RESULT_JSON_INVALID", + "trailing JSON", +); +assertCode( + validateUnboundResultEnvelopeBytes(Buffer.from("[]")), + "RESULT_ROOT_INVALID", + "array root", +); +assertCode( + validateUnboundResultEnvelopeBytes(Buffer.from( + "{\"schema\":\"clawsec.result/v1\",\"schema\":\"clawsec.result/v1\"}", + )), + "RESULT_DUPLICATE_KEY", + "duplicate key", +); + +const deterministicInput = Buffer.from("{\"duplicate\":1,\"duplicate\":2}"); +assert.deepEqual( + validateUnboundResultEnvelopeBytes(deterministicInput), + validateUnboundResultEnvelopeBytes(deterministicInput), + "diagnostics must be deterministic", +); + +const controlKeyDuplicate = Buffer.from( + "{\"safe\":1,\"line\\nbreak\":1,\"line\\nbreak\":2}", +); +const controlKeyResult = validateUnboundResultEnvelopeBytes(controlKeyDuplicate); +assertCode(controlKeyResult, "RESULT_DUPLICATE_KEY", "control-key duplicate"); +assert( + controlKeyResult.errors.every((entry) => ( + !entry.path.includes("\n") && !entry.message.includes("\n") + )), + `duplicate-key diagnostic permits line injection: ${JSON.stringify(controlKeyResult)}`, +); + +const controlProperty = clone(doctorPass); +controlProperty["line\nbreak"] = true; +const controlPropertyResult = validateUnboundResultEnvelopeBytes(encode(controlProperty)); +assertCode(controlPropertyResult, "RESULT_SCHEMA_INVALID", "control property"); +assert( + controlPropertyResult.errors.every((entry) => ( + !entry.path.includes("\n") && !entry.message.includes("\n") + )), + `schema diagnostic permits line injection: ${JSON.stringify(controlPropertyResult)}`, +); + +const diagnosticFanout = clone(doctorPass); +for (let index = 0; index < 5000; index += 1) { + diagnosticFanout[`unknown_${index.toString().padStart(4, "0")}`] = true; +} +const diagnosticFanoutResult = + validateUnboundResultEnvelopeBytes(encode(diagnosticFanout)); +assert.equal(diagnosticFanoutResult.valid, false); +assert( + diagnosticFanoutResult.errors.length <= 64, + `diagnostic cap exceeded: ${diagnosticFanoutResult.errors.length}`, +); +assert( + diagnosticFanoutResult.errors.some((entry) => ( + entry.code === "RESULT_DIAGNOSTICS_TRUNCATED" + )), + "diagnostic fanout was not marked as truncated", +); + +const deeplyNestedInput = Buffer.from( + `{"deep":${"[".repeat(20000)}0${"]".repeat(20000)}}`, +); +assertCode( + validateUnboundResultEnvelopeBytes(deeplyNestedInput), + "RESULT_NESTING_TOO_DEEP", + "deep JSON input", +); + +const oversizedInput = Buffer.alloc((1024 * 1024) + 1, 0x20); +assertCode( + validateUnboundResultEnvelopeBytes(oversizedInput), + "RESULT_TOO_LARGE", + "oversized JSON input", +); + +const multipleErrors = clone(doctorPass); +multipleErrors.reason_code = "policy_blocked"; +multipleErrors.invocation.harness.name = "hermes"; +multipleErrors.invocation.scope = { + kind: "openclaw.workspace", + ref: "workspace-01", +}; +const multipleErrorResult = validateUnboundResultEnvelope(multipleErrors); +assert.deepEqual( + multipleErrorResult.errors, + [...multipleErrorResult.errors].sort(compareDiagnostics), + "diagnostics must be sorted", +); + +for (const guardedRoot of [ + path.join(repositoryRoot, "contracts"), + path.join(repositoryRoot, "scripts/ci"), +]) { + const appleDoubleFiles = await findAppleDoubleFiles(guardedRoot); + assert.deepEqual( + appleDoubleFiles, + [], + `AppleDouble files are forbidden: ${appleDoubleFiles.join(", ")}`, + ); +} + +process.stdout.write( + "PASS result-envelope contract: 2 committed fixtures, 3 roles, 4 harnesses, 8 outcomes, " + + "exact metadata binding, invocation binding, parser hardening, and negative cases\n", +); + +function makeResult({ + executor = coreNanoclawRef, + operation = "core.doctor", + subject = { kind: "executor" }, + outcome = "pass", + reasonCode = "operation_completed", + summary = "The executor reports that the operation completed.", + effects = [], +} = {}) { + return { + schema: "clawsec.result/v1", + executor: clone(executor), + subject: clone(subject), + invocation: { + id: "55555555-5555-4555-8555-555555555555", + operation, + harness: { + name: executor.harness, + version: executor.harness === "nanoclaw" ? "2.1.17" : "1.0.0", + }, + scope: defaultScope(executor.harness), + }, + reported_at: "2026-07-23T11:00:00Z", + outcome, + reason_code: reasonCode, + summary, + effects: clone(effects), + }; +} + +function defaultScope(harness) { + const scopes = { + hermes: { + kind: "hermes.profile", + ref: "fixture-profile", + }, + nanoclaw: { + kind: "nanoclaw.checkout", + ref: "nanoclaw-v2-lab-01", + }, + openclaw: { + kind: "openclaw.workspace", + ref: "fixture-workspace", + }, + picoclaw: { + kind: "picoclaw.home", + ref: "fixture-home", + }, + }; + return clone(scopes[harness]); +} + +function effect( + effectId, + state, + targetValue = ".claude/skills/clawsec-suite-nanoclaw", +) { + return { + effect_id: effectId, + state, + target: { + kind: "relative_path", + value: targetValue, + }, + summary: `Executor reports effect ${effectId}.`, + }; +} + +function componentRefFromMetadata(bytes) { + const skill = JSON.parse(bytes); + const reference = { + schema: "clawsec.component-ref/v1", + name: skill.name, + version: skill.version, + harness: skill.clawsec.supported_harness.name, + role: skill.clawsec.role, + metadata_digest: digestBytes(bytes), + }; + if (skill.clawsec.family !== undefined) { + reference.family = skill.clawsec.family; + } + return reference; +} + +function applyFixtureHarness(document, metadataBytes, scope) { + const skill = JSON.parse(metadataBytes); + document.invocation.harness.version = + skill.clawsec.supported_harness.minimum_version; + document.invocation.scope = scope; + return document; +} + +function validateBound( + document, + metadataValues, + expectedContext = expectedContextFor(document), +) { + return validateBoundResultEnvelope({ + resultBytes: encode(document), + metadataBytesByDigest: metadataMap(...metadataValues), + expectedContext, + }); +} + +function expectedContextFor(document) { + return { + executor: clone(document.executor), + subject: clone(document.subject), + invocation: clone(document.invocation), + }; +} + +function metadataMap(...metadataValues) { + return new Map(metadataValues.map((bytes) => [digestBytes(bytes), bytes])); +} + +function encode(value) { + return Buffer.from(`${JSON.stringify(value)}\n`); +} + +function clone(value) { + return JSON.parse(JSON.stringify(value)); +} + +function assertValid(result, label) { + assert.equal( + result.valid, + true, + `${label} should be valid: ${JSON.stringify(result.errors)}`, + ); +} + +function assertCode(result, expectedCode, label) { + assert.equal(result.valid, false, `${label} should be invalid`); + assert( + result.errors.some((entry) => entry.code === expectedCode), + `${label} missing ${expectedCode}: ${JSON.stringify(result.errors)}`, + ); +} + +function compareDiagnostics(left, right) { + return compareAscii(left.code, right.code) + || compareAscii(left.path, right.path) + || compareAscii(left.message, right.message); +} + +function compareAscii(left, right) { + if (left < right) return -1; + if (left > right) return 1; + return 0; +} + +async function findAppleDoubleFiles(directory) { + const matches = []; + const entries = await readdir(directory, { withFileTypes: true }); + for (const entry of entries) { + const entryPath = path.join(directory, entry.name); + if (entry.name.startsWith("._")) matches.push(entryPath); + if (entry.isDirectory()) matches.push(...await findAppleDoubleFiles(entryPath)); + } + return matches.sort(); +}