@b00dea/cockpit 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +474 -0
- package/LICENSE +21 -0
- package/README.md +25 -2
- package/bin/cockpit.mjs +33 -0
- package/cockpit.schema.json +2644 -0
- package/dist/autonomy.d.ts +45 -0
- package/dist/autonomy.js +173 -0
- package/dist/bounded-read.d.ts +14 -0
- package/dist/bounded-read.js +42 -0
- package/dist/browser/commands.d.ts +117 -0
- package/dist/browser/commands.js +453 -0
- package/dist/browser/package-paths.d.ts +1 -0
- package/dist/browser/package-paths.js +6 -0
- package/dist/browser/proxy.d.ts +22 -0
- package/dist/browser/proxy.js +37 -0
- package/dist/browser/runtime.d.ts +118 -0
- package/dist/browser/runtime.js +186 -0
- package/dist/browser/surfaces.d.ts +141 -0
- package/dist/browser/surfaces.js +561 -0
- package/dist/browser/ui-gate.d.ts +15 -0
- package/dist/browser/ui-gate.js +18 -0
- package/dist/canonical-json.d.ts +2 -0
- package/dist/canonical-json.js +5 -0
- package/dist/cli-guard.d.ts +5 -0
- package/dist/cli-guard.js +71 -0
- package/dist/cli-main.d.ts +176 -0
- package/dist/cli-main.js +1584 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +16 -0
- package/dist/command-table.d.ts +121 -0
- package/dist/command-table.js +664 -0
- package/dist/config/load.d.ts +183 -0
- package/dist/config/load.js +367 -0
- package/dist/config/sibling-toml.d.ts +48 -0
- package/dist/config/sibling-toml.js +160 -0
- package/dist/config/surface-tools-toml.d.ts +9 -0
- package/dist/config/surface-tools-toml.js +37 -0
- package/dist/config/unconfirmed-toml.d.ts +9 -0
- package/dist/config/unconfirmed-toml.js +29 -0
- package/dist/config/verified-write.d.ts +43 -0
- package/dist/config/verified-write.js +89 -0
- package/dist/diagnostics.d.ts +26 -0
- package/dist/diagnostics.js +344 -0
- package/dist/docs/check.d.ts +16 -0
- package/dist/docs/check.js +197 -0
- package/dist/docs/decision-record.d.ts +34 -0
- package/dist/docs/decision-record.js +135 -0
- package/dist/docs/decisions.d.ts +16 -0
- package/dist/docs/decisions.js +103 -0
- package/dist/docs/markdown.d.ts +36 -0
- package/dist/docs/markdown.js +88 -0
- package/dist/docs/references.d.ts +32 -0
- package/dist/docs/references.js +148 -0
- package/dist/emit.d.ts +22 -0
- package/dist/emit.js +43 -0
- package/dist/entrypoint-kinds.d.ts +3 -0
- package/dist/entrypoint-kinds.js +11 -0
- package/dist/environment/application/confirm.d.ts +35 -0
- package/dist/environment/application/confirm.js +122 -0
- package/dist/environment/application/destructive.d.ts +6 -0
- package/dist/environment/application/destructive.js +429 -0
- package/dist/environment/application/doctor.d.ts +94 -0
- package/dist/environment/application/doctor.js +247 -0
- package/dist/environment/application/guard.d.ts +49 -0
- package/dist/environment/application/guard.js +677 -0
- package/dist/environment/application/health.d.ts +143 -0
- package/dist/environment/application/health.js +385 -0
- package/dist/environment/application/lifecycle-context.d.ts +235 -0
- package/dist/environment/application/lifecycle-context.js +154 -0
- package/dist/environment/application/lifecycle-state.d.ts +25 -0
- package/dist/environment/application/lifecycle-state.js +202 -0
- package/dist/environment/application/lifecycle.d.ts +18 -0
- package/dist/environment/application/lifecycle.js +737 -0
- package/dist/environment/application/logs.d.ts +45 -0
- package/dist/environment/application/logs.js +127 -0
- package/dist/environment/application/services.d.ts +60 -0
- package/dist/environment/application/services.js +89 -0
- package/dist/environment/application/setup.d.ts +129 -0
- package/dist/environment/application/setup.js +741 -0
- package/dist/environment/application/shared.d.ts +11 -0
- package/dist/environment/application/shared.js +61 -0
- package/dist/environment/application/siblings.d.ts +43 -0
- package/dist/environment/application/siblings.js +269 -0
- package/dist/environment/claims.d.ts +389 -0
- package/dist/environment/claims.js +1701 -0
- package/dist/environment/codex-hook-trust.d.ts +11 -0
- package/dist/environment/codex-hook-trust.js +185 -0
- package/dist/environment/compose-environment.d.ts +1 -0
- package/dist/environment/compose-environment.js +6 -0
- package/dist/environment/compose-ownership.d.ts +21 -0
- package/dist/environment/compose-ownership.js +117 -0
- package/dist/environment/compose-topology.d.ts +125 -0
- package/dist/environment/compose-topology.js +490 -0
- package/dist/environment/contract.d.ts +522 -0
- package/dist/environment/contract.js +794 -0
- package/dist/environment/declaration.d.ts +14 -0
- package/dist/environment/declaration.js +39 -0
- package/dist/environment/destructive-composition.d.ts +29 -0
- package/dist/environment/destructive-composition.js +333 -0
- package/dist/environment/docker-daemon.d.ts +30 -0
- package/dist/environment/docker-daemon.js +78 -0
- package/dist/environment/docker-endpoint.d.ts +10 -0
- package/dist/environment/docker-endpoint.js +19 -0
- package/dist/environment/env-schema-observation.d.ts +46 -0
- package/dist/environment/env-schema-observation.js +241 -0
- package/dist/environment/evidence.d.ts +136 -0
- package/dist/environment/evidence.js +437 -0
- package/dist/environment/executable.d.ts +38 -0
- package/dist/environment/executable.js +190 -0
- package/dist/environment/generated-application/authorities.d.ts +5 -0
- package/dist/environment/generated-application/authorities.js +61 -0
- package/dist/environment/generated-application/bootstrap-skill.d.ts +16 -0
- package/dist/environment/generated-application/bootstrap-skill.js +328 -0
- package/dist/environment/generated-application/check.d.ts +13 -0
- package/dist/environment/generated-application/check.js +191 -0
- package/dist/environment/generated-application/custom-surface.d.ts +26 -0
- package/dist/environment/generated-application/custom-surface.js +289 -0
- package/dist/environment/generated-application/prepare.d.ts +29 -0
- package/dist/environment/generated-application/prepare.js +278 -0
- package/dist/environment/generated-application/publication.d.ts +33 -0
- package/dist/environment/generated-application/publication.js +547 -0
- package/dist/environment/generated-application/shared.d.ts +492 -0
- package/dist/environment/generated-application/shared.js +354 -0
- package/dist/environment/generated-application/surface-observation.d.ts +4 -0
- package/dist/environment/generated-application/surface-observation.js +69 -0
- package/dist/environment/generated-application/surface-outputs.d.ts +71 -0
- package/dist/environment/generated-application/surface-outputs.js +400 -0
- package/dist/environment/generated-application/surfaces.d.ts +3 -0
- package/dist/environment/generated-application/surfaces.js +704 -0
- package/dist/environment/generated-application/sync.d.ts +6 -0
- package/dist/environment/generated-application/sync.js +124 -0
- package/dist/environment/git-observation.d.ts +202 -0
- package/dist/environment/git-observation.js +553 -0
- package/dist/environment/health-composition.d.ts +12 -0
- package/dist/environment/health-composition.js +645 -0
- package/dist/environment/host-observation.d.ts +56 -0
- package/dist/environment/host-observation.js +52 -0
- package/dist/environment/identity.d.ts +48 -0
- package/dist/environment/identity.js +85 -0
- package/dist/environment/init-application.d.ts +333 -0
- package/dist/environment/init-application.js +726 -0
- package/dist/environment/install-policy-observation.d.ts +71 -0
- package/dist/environment/install-policy-observation.js +284 -0
- package/dist/environment/install-policy.d.ts +48 -0
- package/dist/environment/install-policy.js +96 -0
- package/dist/environment/instance.d.ts +33 -0
- package/dist/environment/instance.js +122 -0
- package/dist/environment/instruction-pointer.d.ts +19 -0
- package/dist/environment/instruction-pointer.js +144 -0
- package/dist/environment/lifecycle-composition.d.ts +31 -0
- package/dist/environment/lifecycle-composition.js +742 -0
- package/dist/environment/lifecycle-stage.d.ts +120 -0
- package/dist/environment/lifecycle-stage.js +408 -0
- package/dist/environment/lifecycle.d.ts +75 -0
- package/dist/environment/lifecycle.js +354 -0
- package/dist/environment/logs.d.ts +132 -0
- package/dist/environment/logs.js +519 -0
- package/dist/environment/map-limit.d.ts +2 -0
- package/dist/environment/map-limit.js +13 -0
- package/dist/environment/mcp-observation.d.ts +9 -0
- package/dist/environment/mcp-observation.js +46 -0
- package/dist/environment/observations.d.ts +46 -0
- package/dist/environment/observations.js +346 -0
- package/dist/environment/operator-application.d.ts +79 -0
- package/dist/environment/operator-application.js +197 -0
- package/dist/environment/ports.d.ts +182 -0
- package/dist/environment/ports.js +1205 -0
- package/dist/environment/posture-observation.d.ts +68 -0
- package/dist/environment/posture-observation.js +253 -0
- package/dist/environment/presence.d.ts +4 -0
- package/dist/environment/presence.js +26 -0
- package/dist/environment/read-composition.d.ts +32 -0
- package/dist/environment/read-composition.js +397 -0
- package/dist/environment/readiness-composition.d.ts +57 -0
- package/dist/environment/readiness-composition.js +151 -0
- package/dist/environment/reclaim.d.ts +370 -0
- package/dist/environment/reclaim.js +1940 -0
- package/dist/environment/repository-identity.d.ts +13 -0
- package/dist/environment/repository-identity.js +108 -0
- package/dist/environment/runtime-measurement-record.d.ts +38 -0
- package/dist/environment/runtime-measurement-record.js +111 -0
- package/dist/environment/runtime-measurement.d.ts +27 -0
- package/dist/environment/runtime-measurement.js +224 -0
- package/dist/environment/runtime-probe-residue.d.ts +43 -0
- package/dist/environment/runtime-probe-residue.js +148 -0
- package/dist/environment/runtime-probe.d.ts +72 -0
- package/dist/environment/runtime-probe.js +725 -0
- package/dist/environment/runtime.d.ts +551 -0
- package/dist/environment/runtime.js +2405 -0
- package/dist/environment/seed.d.ts +16 -0
- package/dist/environment/seed.js +43 -0
- package/dist/environment/selftest-application.d.ts +86 -0
- package/dist/environment/selftest-application.js +713 -0
- package/dist/environment/sensitive.d.ts +3 -0
- package/dist/environment/sensitive.js +48 -0
- package/dist/environment/served-checkout-grants.d.ts +53 -0
- package/dist/environment/served-checkout-grants.js +182 -0
- package/dist/environment/setup-observation.d.ts +62 -0
- package/dist/environment/setup-observation.js +1473 -0
- package/dist/environment/setup.d.ts +43 -0
- package/dist/environment/setup.js +176 -0
- package/dist/environment/sibling-checkouts.d.ts +119 -0
- package/dist/environment/sibling-checkouts.js +254 -0
- package/dist/environment/sibling-instructions.d.ts +33 -0
- package/dist/environment/sibling-instructions.js +224 -0
- package/dist/environment/sibling-observation.d.ts +27 -0
- package/dist/environment/sibling-observation.js +45 -0
- package/dist/environment/sibling-pull.d.ts +23 -0
- package/dist/environment/sibling-pull.js +95 -0
- package/dist/environment/sibling-registration.d.ts +45 -0
- package/dist/environment/sibling-registration.js +538 -0
- package/dist/environment/stack-inputs.d.ts +18 -0
- package/dist/environment/stack-inputs.js +210 -0
- package/dist/environment/stack-inventory-prune.d.ts +31 -0
- package/dist/environment/stack-inventory-prune.js +129 -0
- package/dist/environment/stack-inventory-report.d.ts +44 -0
- package/dist/environment/stack-inventory-report.js +78 -0
- package/dist/environment/stack-inventory.d.ts +94 -0
- package/dist/environment/stack-inventory.js +418 -0
- package/dist/environment/state.d.ts +410 -0
- package/dist/environment/state.js +2153 -0
- package/dist/environment/templates.d.ts +140 -0
- package/dist/environment/templates.js +685 -0
- package/dist/environment/tool-registration.d.ts +9 -0
- package/dist/environment/tool-registration.js +92 -0
- package/dist/environment/upgrade-application.d.ts +19 -0
- package/dist/environment/upgrade-application.js +175 -0
- package/dist/environment-exec/execute.d.ts +105 -0
- package/dist/environment-exec/execute.js +1179 -0
- package/dist/environment-exec/libexec-program.d.ts +6 -0
- package/dist/environment-exec/libexec-program.js +22 -0
- package/dist/environment-exec/verified-digest.d.ts +4 -0
- package/dist/environment-exec/verified-digest.js +16 -0
- package/dist/exact-keys.d.ts +7 -0
- package/dist/exact-keys.js +15 -0
- package/dist/fix-catalogue.d.ts +32 -0
- package/dist/fix-catalogue.js +60 -0
- package/dist/freshness.d.ts +29 -0
- package/dist/freshness.js +75 -0
- package/dist/generated-environment/adopt.d.ts +16 -0
- package/dist/generated-environment/adopt.js +271 -0
- package/dist/generated-environment/command.d.ts +47 -0
- package/dist/generated-environment/command.js +75 -0
- package/dist/generated-environment/transaction.d.ts +186 -0
- package/dist/generated-environment/transaction.js +1770 -0
- package/dist/grading/evaluate.d.ts +71 -0
- package/dist/grading/evaluate.js +403 -0
- package/dist/grading/grade.d.ts +33 -0
- package/dist/grading/grade.js +57 -0
- package/dist/guards/answer-confirmation.d.ts +41 -0
- package/dist/guards/answer-confirmation.js +109 -0
- package/dist/guards/evaluator.d.ts +130 -0
- package/dist/guards/evaluator.js +1258 -0
- package/dist/guards/harness-adapter.d.ts +17 -0
- package/dist/guards/harness-adapter.js +57 -0
- package/dist/guards/host.d.ts +8 -0
- package/dist/guards/host.js +10 -0
- package/dist/guards/invocation-parser.d.ts +66 -0
- package/dist/guards/invocation-parser.js +595 -0
- package/dist/guards/local-identity.d.ts +44 -0
- package/dist/guards/local-identity.js +129 -0
- package/dist/guards/policy-change.d.ts +43 -0
- package/dist/guards/policy-change.js +159 -0
- package/dist/guards/policy-pin.d.ts +13 -0
- package/dist/guards/policy-pin.js +177 -0
- package/dist/guards/protected-locations.d.ts +62 -0
- package/dist/guards/protected-locations.js +461 -0
- package/dist/guards/read-only.d.ts +37 -0
- package/dist/guards/read-only.js +315 -0
- package/dist/init/command.d.ts +41 -0
- package/dist/init/command.js +330 -0
- package/dist/init/derive.d.ts +53 -0
- package/dist/init/derive.js +819 -0
- package/dist/init/draft.d.ts +140 -0
- package/dist/init/draft.js +1228 -0
- package/dist/init/render.d.ts +47 -0
- package/dist/init/render.js +104 -0
- package/dist/init/scaffold.d.ts +53 -0
- package/dist/init/scaffold.js +121 -0
- package/dist/invocation.d.ts +13 -0
- package/dist/invocation.js +20 -0
- package/dist/package-metadata.d.ts +9 -0
- package/dist/package-metadata.js +19 -0
- package/dist/readiness.d.ts +53 -0
- package/dist/readiness.js +88 -0
- package/dist/references/named.d.ts +24 -0
- package/dist/references/named.js +27 -0
- package/dist/refusal.d.ts +15 -0
- package/dist/refusal.js +7 -0
- package/dist/registry-derivation.d.ts +31 -0
- package/dist/registry-derivation.js +32 -0
- package/dist/registry-resolve/command.d.ts +47 -0
- package/dist/registry-resolve/command.js +226 -0
- package/dist/registry-resolve/registry.d.ts +68 -0
- package/dist/registry-resolve/registry.js +292 -0
- package/dist/registry-resolve/render-shell.d.ts +18 -0
- package/dist/registry-resolve/render-shell.js +36 -0
- package/dist/repair.d.ts +29 -0
- package/dist/repair.js +8 -0
- package/dist/repository-path.d.ts +9 -0
- package/dist/repository-path.js +16 -0
- package/dist/schema-json.d.ts +1 -0
- package/dist/schema-json.js +177 -0
- package/dist/schema.d.ts +1289 -0
- package/dist/schema.js +985 -0
- package/dist/setup/command.d.ts +4 -0
- package/dist/setup/command.js +4 -0
- package/dist/sha256.d.ts +1 -0
- package/dist/sha256.js +4 -0
- package/dist/surfaces/adoption.d.ts +41 -0
- package/dist/surfaces/adoption.js +217 -0
- package/dist/surfaces/consumer-instructions.d.ts +20 -0
- package/dist/surfaces/consumer-instructions.js +43 -0
- package/dist/surfaces/contract.d.ts +42 -0
- package/dist/surfaces/contract.js +169 -0
- package/dist/surfaces/core-declarations.d.ts +24 -0
- package/dist/surfaces/core-declarations.js +79 -0
- package/dist/surfaces/development-markers.d.ts +1 -0
- package/dist/surfaces/development-markers.js +18 -0
- package/dist/surfaces/generator-adapter.d.ts +71 -0
- package/dist/surfaces/generator-adapter.js +809 -0
- package/dist/surfaces/harness-registry.d.ts +260 -0
- package/dist/surfaces/harness-registry.js +146 -0
- package/dist/surfaces/mcp.d.ts +145 -0
- package/dist/surfaces/mcp.js +349 -0
- package/dist/surfaces/merge.d.ts +30 -0
- package/dist/surfaces/merge.js +105 -0
- package/dist/surfaces/ownership.d.ts +190 -0
- package/dist/surfaces/ownership.js +795 -0
- package/dist/surfaces/projection.d.ts +136 -0
- package/dist/surfaces/projection.js +716 -0
- package/dist/surfaces/render.d.ts +79 -0
- package/dist/surfaces/render.js +636 -0
- package/dist/surfaces/restrictions.d.ts +130 -0
- package/dist/surfaces/restrictions.js +447 -0
- package/dist/surfaces/selftest.d.ts +162 -0
- package/dist/surfaces/selftest.js +393 -0
- package/dist/surfaces/skill-contract.d.ts +85 -0
- package/dist/surfaces/skill-contract.js +449 -0
- package/dist/surfaces/sync.d.ts +57 -0
- package/dist/surfaces/sync.js +164 -0
- package/dist/team-metadata.d.ts +41 -0
- package/dist/team-metadata.js +513 -0
- package/dist/testing/gates.d.ts +91 -0
- package/dist/testing/gates.js +182 -0
- package/dist/testing/hooks.d.ts +59 -0
- package/dist/testing/hooks.js +139 -0
- package/dist/testing/test-protection.d.ts +34 -0
- package/dist/testing/test-protection.js +137 -0
- package/dist/testing/verdict.d.ts +29 -0
- package/dist/testing/verdict.js +62 -0
- package/dist/testing/weakening.d.ts +15 -0
- package/dist/testing/weakening.js +30 -0
- package/dist/work-orders/application-closure.d.ts +7 -0
- package/dist/work-orders/application-closure.js +76 -0
- package/dist/work-orders/application-context.d.ts +162 -0
- package/dist/work-orders/application-context.js +137 -0
- package/dist/work-orders/application-decisions.d.ts +27 -0
- package/dist/work-orders/application-decisions.js +121 -0
- package/dist/work-orders/application-grading.d.ts +11 -0
- package/dist/work-orders/application-grading.js +212 -0
- package/dist/work-orders/application.d.ts +6 -0
- package/dist/work-orders/application.js +223 -0
- package/dist/work-orders/composition.d.ts +101 -0
- package/dist/work-orders/composition.js +1202 -0
- package/dist/work-orders/contract.d.ts +77 -0
- package/dist/work-orders/contract.js +140 -0
- package/dist/work-orders/decisions.d.ts +23 -0
- package/dist/work-orders/decisions.js +55 -0
- package/dist/work-orders/store.d.ts +207 -0
- package/dist/work-orders/store.js +291 -0
- package/docs/README.md +68 -0
- package/docs/architecture.md +136 -0
- package/docs/capability-matrix.md +58 -0
- package/docs/cli-reference.md +807 -0
- package/docs/command-list.md +62 -0
- package/docs/compose-patterns.md +99 -0
- package/docs/concepts.md +173 -0
- package/docs/configuration-essentials.md +177 -0
- package/docs/configuration-reference.md +636 -0
- package/docs/docs-check.md +76 -0
- package/docs/environment.md +948 -0
- package/docs/extending.md +43 -0
- package/docs/faq.md +113 -0
- package/docs/getting-started.md +692 -0
- package/docs/harness-surfaces.md +500 -0
- package/docs/joining-a-team.md +76 -0
- package/docs/release-policy.md +49 -0
- package/docs/safety-and-autonomy.md +463 -0
- package/docs/troubleshooting.md +382 -0
- package/docs/tutorial.md +171 -0
- package/docs/work-and-browser.md +420 -0
- package/libexec/browser-operation.cjs +270 -0
- package/libexec/fence-marker.cjs +101 -0
- package/libexec/fence-marker.d.cts +24 -0
- package/libexec/fenced-group-runner.cjs +77 -0
- package/libexec/fenced-supervisor.cjs +137 -0
- package/libexec/generated-filesystem.cjs +731 -0
- package/libexec/install-filesystem.cjs +225 -0
- package/libexec/rulesync-generator.NOTICE.md +1112 -0
- package/libexec/rulesync-generator.cjs +743 -0
- package/libexec/surface-generator-worker.cjs +266 -0
- package/libexec/surface-output-reader.cjs +99 -0
- package/libexec/surface-scratch-cleanup.cjs +14 -0
- package/libexec/validate-harness-evidence.d.mts +183 -0
- package/libexec/validate-harness-evidence.mjs +1312 -0
- package/package.json +74 -4
- package/surfaces/README.md +33 -0
- package/surfaces/agents/evaluator.agent.md +76 -0
- package/surfaces/guards/bash-guard.sh +48 -0
- package/surfaces/guards/claude.adapter.json +25 -0
- package/surfaces/guards/codex.adapter.json +24 -0
- package/surfaces/guards/opencode.adapter.json +25 -0
- package/surfaces/references/browser.md +63 -0
- package/surfaces/references/test-data-and-personas.md +84 -0
- package/surfaces/references/testing-tiers.md +11 -0
- package/surfaces/skills/cockpit-browser.skill.md +59 -0
- package/surfaces/skills/cockpit-explain.skill.md +32 -0
- package/surfaces/skills/cockpit-fix.skill.md +37 -0
- package/surfaces/skills/cockpit-setup.skill.md +39 -0
- package/surfaces/skills/cockpit-work.skill.md +34 -0
- package/surfaces/skills/collection.json +5 -0
- package/surfaces/skills/procedures/explain.md +66 -0
- package/surfaces/skills/procedures/fix.md +95 -0
- package/surfaces/skills/procedures/setup.md +452 -0
- package/surfaces/skills/procedures/work.md +134 -0
|
@@ -0,0 +1,636 @@
|
|
|
1
|
+
import { sha256Hex } from '../sha256.js';
|
|
2
|
+
import { isAbsolute } from 'node:path';
|
|
3
|
+
import { parse as parseToml, stringify as stringifyToml } from 'smol-toml';
|
|
4
|
+
import { snapshotGeneratedJson, tryParseJson } from '../environment/contract.js';
|
|
5
|
+
import { CustomHarness } from '../schema.js';
|
|
6
|
+
import { safeSurfacePath as safePath } from './contract.js';
|
|
7
|
+
import { GUARD_SURFACE_TARGETS, SURFACE_TARGETS, harnessFact } from './harness-registry.js';
|
|
8
|
+
import { RULESYNC_VERSION } from './generator-adapter.js';
|
|
9
|
+
import { stripDevelopmentMarkers } from './development-markers.js';
|
|
10
|
+
import { captureOpenCodeRootMembers, classifySurfaceOwnership, } from './ownership.js';
|
|
11
|
+
import { exactKeys } from '../exact-keys.js';
|
|
12
|
+
const SHA256 = /^[a-f0-9]{64}$/;
|
|
13
|
+
const DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
14
|
+
const MARKDOWN_PATHS = new Set(['AGENTS.md', 'CLAUDE.md']);
|
|
15
|
+
const BUILT_INS = new Set(SURFACE_TARGETS);
|
|
16
|
+
const NATIVE_BUILT_INS = new Set(GUARD_SURFACE_TARGETS);
|
|
17
|
+
const SKILL_REFERENCE_PATH = /^references\/[a-z0-9]+(?:-[a-z0-9]+)*\.md$/;
|
|
18
|
+
const SKILL_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
19
|
+
function object(value) {
|
|
20
|
+
return value !== undefined && value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
21
|
+
}
|
|
22
|
+
export function surfaceModelSha256(value) {
|
|
23
|
+
const captured = snapshotGeneratedJson(value);
|
|
24
|
+
return captured === undefined ? undefined : sha256Hex(JSON.stringify(captured));
|
|
25
|
+
}
|
|
26
|
+
function refuse(code, message, target) {
|
|
27
|
+
return { kind: 'refusal', diagnostic: { code, message, ...(target === undefined ? {} : { target }) } };
|
|
28
|
+
}
|
|
29
|
+
function validFileFields(value) {
|
|
30
|
+
return object(value) && safePath(value.path)
|
|
31
|
+
&& typeof value.bytes === 'string' && Buffer.byteLength(value.bytes) <= 65_536
|
|
32
|
+
&& typeof value.sha256 === 'string' && value.sha256 === sha256Hex(value.bytes);
|
|
33
|
+
}
|
|
34
|
+
function validFile(value) {
|
|
35
|
+
return object(value) && exactKeys(value, ['path', 'bytes', 'sha256']) && validFileFields(value);
|
|
36
|
+
}
|
|
37
|
+
function validEvidence(value) {
|
|
38
|
+
return object(value) && (value.kind === 'guide' || value.kind === 'mcp')
|
|
39
|
+
&& exactKeys(value, ['kind', 'root', 'path', 'sha256', 'identity'])
|
|
40
|
+
&& typeof value.root === 'string' && isAbsolute(value.root) && typeof value.path === 'string' && value.path.length > 0
|
|
41
|
+
&& typeof value.identity === 'string' && value.identity.length > 0
|
|
42
|
+
&& typeof value.sha256 === 'string' && SHA256.test(value.sha256);
|
|
43
|
+
}
|
|
44
|
+
function sorted(values) {
|
|
45
|
+
const result = [...new Set(values)].sort();
|
|
46
|
+
return result.length === 0 ? undefined : result;
|
|
47
|
+
}
|
|
48
|
+
function weakest(values) {
|
|
49
|
+
return values.includes('instructed') ? 'instructed' : 'enforced';
|
|
50
|
+
}
|
|
51
|
+
function sharedScope(contributions) {
|
|
52
|
+
const scopes = new Set(contributions.map(contribution => contribution.scope));
|
|
53
|
+
return scopes.size === 1 ? contributions[0]?.scope : undefined;
|
|
54
|
+
}
|
|
55
|
+
function frontmatter(base, fragment) {
|
|
56
|
+
if (!base.startsWith('---\n'))
|
|
57
|
+
return undefined;
|
|
58
|
+
const close = base.indexOf('\n---\n', 4);
|
|
59
|
+
if (close < 0 || !fragment.endsWith('\n'))
|
|
60
|
+
return undefined;
|
|
61
|
+
const key = fragment.startsWith('disallowedTools:') ? 'disallowedTools' : fragment.startsWith('permission:\n') ? 'permission' : undefined;
|
|
62
|
+
if (!key || new RegExp(`^${key}:`, 'm').test(base.slice(4, close)))
|
|
63
|
+
return undefined;
|
|
64
|
+
return `${base.slice(0, close)}\n${fragment}${base.slice(close + 1)}`;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The composition rule for a hook-registration settings file with several producers: every
|
|
68
|
+
* PreToolUse row is kept once, and every other top-level key and hook event must agree exactly.
|
|
69
|
+
* A refusal names the key so the producer that disagrees can be found.
|
|
70
|
+
*/
|
|
71
|
+
export function composeHookSettings(values) {
|
|
72
|
+
const result = {};
|
|
73
|
+
const hookEvents = {};
|
|
74
|
+
const rows = [];
|
|
75
|
+
const refusal = (key, reason) => ({ kind: 'refusal', key, reason });
|
|
76
|
+
for (const parsed of values) {
|
|
77
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
78
|
+
return refusal('(root)', 'malformed');
|
|
79
|
+
const value = parsed;
|
|
80
|
+
const hooks = value.hooks;
|
|
81
|
+
if (hooks !== undefined && (hooks === null || typeof hooks !== 'object' || Array.isArray(hooks)))
|
|
82
|
+
return refusal('hooks', 'malformed');
|
|
83
|
+
const pre = hooks === undefined ? [] : hooks.PreToolUse ?? [];
|
|
84
|
+
if (!Array.isArray(pre))
|
|
85
|
+
return refusal('hooks.PreToolUse', 'malformed');
|
|
86
|
+
rows.push(...pre);
|
|
87
|
+
for (const [key, item] of Object.entries(value)) {
|
|
88
|
+
if (key === 'hooks')
|
|
89
|
+
continue;
|
|
90
|
+
if (Object.hasOwn(result, key) && JSON.stringify(result[key]) !== JSON.stringify(item))
|
|
91
|
+
return refusal(key, 'divergent');
|
|
92
|
+
result[key] = item;
|
|
93
|
+
}
|
|
94
|
+
for (const [key, item] of Object.entries(hooks ?? {})) {
|
|
95
|
+
if (key === 'PreToolUse')
|
|
96
|
+
continue;
|
|
97
|
+
if (Object.hasOwn(hookEvents, key) && JSON.stringify(hookEvents[key]) !== JSON.stringify(item))
|
|
98
|
+
return refusal(`hooks.${key}`, 'divergent');
|
|
99
|
+
hookEvents[key] = item;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const unique = [...new Map(rows.map(row => [JSON.stringify(row), row])).values()];
|
|
103
|
+
result.hooks = { ...hookEvents, PreToolUse: unique };
|
|
104
|
+
return { kind: 'complete', value: result };
|
|
105
|
+
}
|
|
106
|
+
function hookJson(contributions) {
|
|
107
|
+
const values = [];
|
|
108
|
+
for (const contribution of contributions) {
|
|
109
|
+
const value = tryParseJson(contribution.bytes);
|
|
110
|
+
if (value === undefined)
|
|
111
|
+
return undefined;
|
|
112
|
+
values.push(value);
|
|
113
|
+
}
|
|
114
|
+
const composed = composeHookSettings(values);
|
|
115
|
+
return composed.kind === 'complete' ? `${JSON.stringify(composed.value, null, 2)}\n` : undefined;
|
|
116
|
+
}
|
|
117
|
+
function customReceipts(harnesses, value) {
|
|
118
|
+
if (!Array.isArray(value) || value.length > 384)
|
|
119
|
+
return undefined;
|
|
120
|
+
const available = new Map();
|
|
121
|
+
for (const harness of harnesses)
|
|
122
|
+
for (const capability of harness.capabilities) {
|
|
123
|
+
if (capability.state !== 'unavailable')
|
|
124
|
+
available.set(`${harness.name}\0${capability.feature}`, capability);
|
|
125
|
+
}
|
|
126
|
+
const seen = new Set();
|
|
127
|
+
const contributions = [];
|
|
128
|
+
const evidence = [];
|
|
129
|
+
for (const candidate of value) {
|
|
130
|
+
if (!object(candidate) || !exactKeys(candidate, ['harness', 'feature', 'state', 'measuredOn', 'module', 'export', 'sourceSha256', 'resultSha256', 'files'])
|
|
131
|
+
|| typeof candidate.harness !== 'string' || typeof candidate.feature !== 'string'
|
|
132
|
+
|| candidate.state !== 'enforced' && candidate.state !== 'instructed' || typeof candidate.measuredOn !== 'string' || !DATE.test(candidate.measuredOn)
|
|
133
|
+
|| typeof candidate.module !== 'string' || typeof candidate.export !== 'string'
|
|
134
|
+
|| typeof candidate.sourceSha256 !== 'string' || !SHA256.test(candidate.sourceSha256)
|
|
135
|
+
|| typeof candidate.resultSha256 !== 'string' || !SHA256.test(candidate.resultSha256)
|
|
136
|
+
|| !Array.isArray(candidate.files) || candidate.files.length === 0)
|
|
137
|
+
return undefined;
|
|
138
|
+
const identity = `${candidate.harness}\0${candidate.feature}`;
|
|
139
|
+
const declaration = available.get(identity);
|
|
140
|
+
if (!declaration || seen.has(identity) || declaration.state !== candidate.state || declaration.measured_on !== candidate.measuredOn
|
|
141
|
+
|| declaration.mechanism.module !== candidate.module || declaration.mechanism.export !== candidate.export)
|
|
142
|
+
return undefined;
|
|
143
|
+
const files = [];
|
|
144
|
+
for (const file of candidate.files) {
|
|
145
|
+
if (!object(file) || !exactKeys(file, ['path', 'bytes', 'sha256', 'scope']) || !validFileFields(file)
|
|
146
|
+
|| file.scope !== 'always' && file.scope !== 'scoped')
|
|
147
|
+
return undefined;
|
|
148
|
+
files.push(file);
|
|
149
|
+
}
|
|
150
|
+
if (surfaceModelSha256(files) !== candidate.resultSha256)
|
|
151
|
+
return undefined;
|
|
152
|
+
const receipt = candidate;
|
|
153
|
+
for (const file of files)
|
|
154
|
+
contributions.push({ receipt, file });
|
|
155
|
+
evidence.push({ kind: 'custom-harness', harness: receipt.harness, feature: receipt.feature, state: receipt.state,
|
|
156
|
+
measuredOn: receipt.measuredOn, module: receipt.module, export: receipt.export,
|
|
157
|
+
sourceSha256: receipt.sourceSha256, resultSha256: receipt.resultSha256 });
|
|
158
|
+
seen.add(identity);
|
|
159
|
+
}
|
|
160
|
+
if ([...available.keys()].some(identity => !seen.has(identity)))
|
|
161
|
+
return undefined;
|
|
162
|
+
return { contributions, evidence };
|
|
163
|
+
}
|
|
164
|
+
function nativeFragments(value, expectedDigest, targets) {
|
|
165
|
+
if (!object(value) || !exactKeys(value, ['modelSha256', 'fragments']) || value.modelSha256 !== expectedDigest || !Array.isArray(value.fragments))
|
|
166
|
+
return undefined;
|
|
167
|
+
const result = [];
|
|
168
|
+
const seen = new Set();
|
|
169
|
+
for (const fragment of value.fragments) {
|
|
170
|
+
if (!object(fragment) || !exactKeys(fragment, ['target', 'path', 'baseSha256', 'bytes', 'sha256'])
|
|
171
|
+
|| typeof fragment.target !== 'string' || !BUILT_INS.has(fragment.target) || !targets.has(fragment.target)
|
|
172
|
+
|| typeof fragment.baseSha256 !== 'string' || !SHA256.test(fragment.baseSha256)
|
|
173
|
+
|| !validFileFields(fragment) || seen.has(fragment.target))
|
|
174
|
+
return undefined;
|
|
175
|
+
seen.add(fragment.target);
|
|
176
|
+
result.push(fragment);
|
|
177
|
+
}
|
|
178
|
+
return seen.size === targets.size ? result : undefined;
|
|
179
|
+
}
|
|
180
|
+
function mergeNamedObject(left, right, replace) {
|
|
181
|
+
const first = snapshotGeneratedJson(left);
|
|
182
|
+
const second = snapshotGeneratedJson(right);
|
|
183
|
+
if (!object(first) || !object(second))
|
|
184
|
+
return undefined;
|
|
185
|
+
const merged = { ...first };
|
|
186
|
+
for (const [key, value] of Object.entries(second)) {
|
|
187
|
+
if (Object.hasOwn(merged, key) && key !== replace && JSON.stringify(merged[key]) !== JSON.stringify(value))
|
|
188
|
+
return undefined;
|
|
189
|
+
merged[key] = value;
|
|
190
|
+
}
|
|
191
|
+
return merged;
|
|
192
|
+
}
|
|
193
|
+
function mergeBrowserMcp(path, base, browser) {
|
|
194
|
+
try {
|
|
195
|
+
const first = snapshotGeneratedJson(path === '.codex/config.toml' ? parseToml(base) : JSON.parse(base));
|
|
196
|
+
const second = snapshotGeneratedJson(path === '.codex/config.toml' ? parseToml(browser) : JSON.parse(browser));
|
|
197
|
+
if (!object(first) || !object(second))
|
|
198
|
+
return undefined;
|
|
199
|
+
const key = path === '.mcp.json' ? 'mcpServers' : path === '.codex/config.toml' ? 'mcp_servers' : path === 'opencode.jsonc' ? 'mcp' : undefined;
|
|
200
|
+
if (!key)
|
|
201
|
+
return undefined;
|
|
202
|
+
const browserMembers = second[key];
|
|
203
|
+
if (!object(browserMembers) || Object.keys(browserMembers).some(name => name !== 'playwright'))
|
|
204
|
+
return undefined;
|
|
205
|
+
const members = mergeNamedObject(first[key], browserMembers, 'playwright');
|
|
206
|
+
if (!members || !Object.hasOwn(members, 'playwright'))
|
|
207
|
+
return undefined;
|
|
208
|
+
const additions = { ...second };
|
|
209
|
+
delete additions[key];
|
|
210
|
+
const merged = mergeNamedObject(first, additions);
|
|
211
|
+
if (merged)
|
|
212
|
+
merged[key] = members;
|
|
213
|
+
return merged ? path === '.codex/config.toml' ? stringifyToml(merged) : `${JSON.stringify(merged, null, 2)}\n` : undefined;
|
|
214
|
+
}
|
|
215
|
+
catch { /* unparseable input is malformed; the caller refuses it. */
|
|
216
|
+
return undefined;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
export function renderSurfaceSet(input) {
|
|
220
|
+
const captured = snapshotGeneratedJson(input);
|
|
221
|
+
const required = ['configuredHarnesses', 'merge', 'generated', 'projection', 'mcp', 'restrictions', 'instructions', 'nativeMcp', 'nativeGrants', 'browser', 'browserReference', 'skillReferences', 'testing', 'customHarnesses', 'customReceipts', 'sourceEvidence'];
|
|
222
|
+
if (!object(captured) || !exactKeys(captured, required) || !Array.isArray(captured.configuredHarnesses)
|
|
223
|
+
|| !Array.isArray(captured.customHarnesses) || !Array.isArray(captured.sourceEvidence)
|
|
224
|
+
|| !object(captured.merge) || captured.merge.kind !== 'complete' || !Array.isArray(captured.merge.surfaces)
|
|
225
|
+
|| !Array.isArray(captured.merge.overrides) || !Array.isArray(captured.merge.findings)
|
|
226
|
+
|| !object(captured.generated) || !exactKeys(captured.generated, ['kind', 'status', 'diagnostics', 'mechanism', 'files', ...(Object.hasOwn(captured.generated, 'warnings') ? ['warnings'] : [])])
|
|
227
|
+
|| captured.generated.kind !== 'complete' || captured.generated.status !== 0 || !Array.isArray(captured.generated.diagnostics)
|
|
228
|
+
|| captured.generated.diagnostics.length !== 0 || !object(captured.generated.mechanism)
|
|
229
|
+
|| !exactKeys(captured.generated.mechanism, ['package', 'version', 'role']) || captured.generated.mechanism.package !== 'rulesync'
|
|
230
|
+
|| captured.generated.mechanism.version !== RULESYNC_VERSION || captured.generated.mechanism.role !== 'spelling'
|
|
231
|
+
|| !Array.isArray(captured.generated.files)
|
|
232
|
+
|| !object(captured.projection) || captured.projection.kind !== 'complete' || !Array.isArray(captured.projection.guides)
|
|
233
|
+
|| !Array.isArray(captured.projection.grants)
|
|
234
|
+
|| !object(captured.mcp) || captured.mcp.kind !== 'complete' || !object(captured.mcp.result)
|
|
235
|
+
|| !Array.isArray(captured.mcp.result.servers) || !Array.isArray(captured.mcp.result.wrappers) || !Array.isArray(captured.mcp.result.harnesses)
|
|
236
|
+
|| !object(captured.restrictions) || captured.restrictions.kind !== 'complete' || !validFile(captured.restrictions.sharedGuard)
|
|
237
|
+
|| !Array.isArray(captured.restrictions.targets))
|
|
238
|
+
return refuse('SURFACE_INPUT_INVALID', 'surface render requires exact accepted leaf results');
|
|
239
|
+
const warnings = Object.hasOwn(captured.generated, 'warnings') ? captured.generated.warnings : [];
|
|
240
|
+
const generatedFiles = captured.generated.files;
|
|
241
|
+
let warningBytes = 0;
|
|
242
|
+
if (!Array.isArray(warnings) || warnings.length > 16 || warnings.some(warning => {
|
|
243
|
+
if (!object(warning) || !exactKeys(warning, ['severity', 'code', 'target', 'path', 'message'])
|
|
244
|
+
|| warning.severity !== 'warning' || warning.code !== 'SURFACE_GENERATOR_TRUST_WARNING'
|
|
245
|
+
|| warning.target !== 'claudecode' || warning.path !== '.claude/settings.json'
|
|
246
|
+
|| typeof warning.message !== 'string' || warning.message.length === 0
|
|
247
|
+
|| /[\u0000-\u001f\u007f]/u.test(warning.message))
|
|
248
|
+
return true;
|
|
249
|
+
const bytes = Buffer.byteLength(warning.message, 'utf8');
|
|
250
|
+
warningBytes += bytes;
|
|
251
|
+
return bytes > 4096 || warningBytes > 16384
|
|
252
|
+
|| !generatedFiles.some(file => object(file) && file.target === warning.target && file.path === warning.path);
|
|
253
|
+
}))
|
|
254
|
+
return refuse('SURFACE_INPUT_INVALID', 'generator trust warnings must be bounded and name an emitted native settings file');
|
|
255
|
+
const configured = captured.configuredHarnesses;
|
|
256
|
+
if (configured.length === 0 || configured.some((value, index) => typeof value !== 'string' || index > 0 && configured[index - 1] >= value)) {
|
|
257
|
+
return refuse('SURFACE_INPUT_INVALID', 'configured harness identities must be sorted and unique');
|
|
258
|
+
}
|
|
259
|
+
const configuredSet = new Set(configured);
|
|
260
|
+
const parsedHarnesses = CustomHarness.array().safeParse(captured.customHarnesses);
|
|
261
|
+
const customNames = new Set(parsedHarnesses.success ? parsedHarnesses.data.map(harness => harness.name) : []);
|
|
262
|
+
// The listed built-in tools: any non-empty subset that includes a guarded one, as the schema admits.
|
|
263
|
+
const listedNative = new Set([...NATIVE_BUILT_INS].filter(target => configuredSet.has(target)));
|
|
264
|
+
if (!parsedHarnesses.success || parsedHarnesses.data.some(harness => !configuredSet.has(harness.name))
|
|
265
|
+
|| [...configuredSet].some(name => !customNames.has(name) && !BUILT_INS.has(name))
|
|
266
|
+
|| listedNative.size === 0)
|
|
267
|
+
return refuse('SURFACE_INPUT_INVALID', 'custom and built-in harness identities do not match the configured set');
|
|
268
|
+
const custom = customReceipts(parsedHarnesses.data, captured.customReceipts);
|
|
269
|
+
if (!custom)
|
|
270
|
+
return refuse('SURFACE_INPUT_INVALID', 'custom capability receipt is missing or does not match its authenticated declaration');
|
|
271
|
+
const browser = captured.browser === null ? null : object(captured.browser)
|
|
272
|
+
&& exactKeys(captured.browser, ['rows', 'files', 'mcp', 'clients', 'graderBrowserAccess'])
|
|
273
|
+
&& captured.browser.graderBrowserAccess === 'retained' && Array.isArray(captured.browser.rows)
|
|
274
|
+
&& Array.isArray(captured.browser.files) && Array.isArray(captured.browser.clients)
|
|
275
|
+
&& object(captured.browser.mcp) ? captured.browser : undefined;
|
|
276
|
+
if (browser === undefined || (browser === null) !== (captured.browserReference === null)
|
|
277
|
+
|| captured.browserReference !== null && (typeof captured.browserReference !== 'string'
|
|
278
|
+
|| Buffer.byteLength(captured.browserReference) === 0 || Buffer.byteLength(captured.browserReference) > 65_536)) {
|
|
279
|
+
return refuse('SURFACE_INPUT_INVALID', 'browser projection and authenticated reference must be paired bounded inputs');
|
|
280
|
+
}
|
|
281
|
+
const skillReferences = Array.isArray(captured.skillReferences) && captured.skillReferences.length <= 16
|
|
282
|
+
&& captured.skillReferences.every(row => object(row) && exactKeys(row, ['skill', 'path', 'bytes'])
|
|
283
|
+
&& typeof row.skill === 'string' && SKILL_NAME.test(row.skill) && typeof row.path === 'string' && SKILL_REFERENCE_PATH.test(row.path)
|
|
284
|
+
&& typeof row.bytes === 'string' && Buffer.byteLength(row.bytes) > 0 && Buffer.byteLength(row.bytes) <= 65_536)
|
|
285
|
+
&& new Set(captured.skillReferences.map(row => JSON.stringify([row.skill, row.path]))).size === captured.skillReferences.length
|
|
286
|
+
? captured.skillReferences : undefined;
|
|
287
|
+
if (!skillReferences)
|
|
288
|
+
return refuse('SURFACE_INPUT_INVALID', 'skill references must be unique bounded references/<name>.md files owned by a named skill');
|
|
289
|
+
if (browser) {
|
|
290
|
+
const browserMcp = browser.mcp;
|
|
291
|
+
const browserServers = object(browserMcp) && browserMcp.kind === 'complete' && object(browserMcp.result)
|
|
292
|
+
&& Array.isArray(browserMcp.result.servers) ? browserMcp.result.servers : undefined;
|
|
293
|
+
const projectedPlaywright = browserServers?.length === 1 && object(browserServers[0]) && browserServers[0].name === 'playwright'
|
|
294
|
+
? browserServers[0] : undefined;
|
|
295
|
+
const sourcePlaywright = captured.mcp.result.servers.find(server => object(server) && server.name === 'playwright');
|
|
296
|
+
const unavailable = browserServers?.length === 0 && !sourcePlaywright
|
|
297
|
+
&& Array.isArray(browser.rows) && Array.isArray(browser.files)
|
|
298
|
+
&& browser.rows.every(row => object(row) && (row.transport !== 'mcp'
|
|
299
|
+
|| row.declaration === 'unavailable' && row.callable === 'unavailable'))
|
|
300
|
+
&& browser.files.every(row => object(row) && !['.mcp.json', '.codex/config.toml', 'opencode.jsonc'].includes(String(row.path)));
|
|
301
|
+
if (!unavailable && (!projectedPlaywright || !sourcePlaywright || JSON.stringify(projectedPlaywright) !== JSON.stringify(sourcePlaywright))) {
|
|
302
|
+
return refuse('SURFACE_INPUT_INVALID', 'browser MCP projection is not bound to the accepted Playwright declaration');
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
const testing = captured.testing === null ? null : object(captured.testing)
|
|
306
|
+
&& exactKeys(captured.testing, ['tierGuide', 'nativeHooks']) && object(captured.testing.tierGuide)
|
|
307
|
+
&& exactKeys(captured.testing.tierGuide, ['kind', 'bytes', 'lines', 'costs'])
|
|
308
|
+
&& captured.testing.tierGuide.kind === 'complete' && typeof captured.testing.tierGuide.bytes === 'string'
|
|
309
|
+
&& Buffer.byteLength(captured.testing.tierGuide.bytes) <= 65_536 && object(captured.testing.nativeHooks)
|
|
310
|
+
? captured.testing : undefined;
|
|
311
|
+
if (testing === undefined)
|
|
312
|
+
return refuse('SURFACE_INPUT_INVALID', 'testing projection must be an exact bounded accepted leaf result');
|
|
313
|
+
const tierGuide = testing && object(testing.tierGuide) ? testing.tierGuide : null;
|
|
314
|
+
const nativeHooks = testing && object(testing.nativeHooks) ? testing.nativeHooks : null;
|
|
315
|
+
if (testing && (!tierGuide || !nativeHooks))
|
|
316
|
+
return refuse('SURFACE_INPUT_INVALID', 'testing projection must retain its validated objects');
|
|
317
|
+
if (tierGuide && (typeof tierGuide.bytes !== 'string' || typeof tierGuide.lines !== 'number' || !Number.isSafeInteger(tierGuide.lines) || tierGuide.lines < 1
|
|
318
|
+
|| tierGuide.lines !== tierGuide.bytes.trimEnd().split('\n').length
|
|
319
|
+
|| !Array.isArray(tierGuide.costs) || tierGuide.costs.length !== listedNative.size
|
|
320
|
+
|| tierGuide.costs.some(cost => !object(cost) || !exactKeys(cost, ['target', 'status', 'metric', 'value', 'conditionalValue', 'cap', 'measuredOn', 'provenance', 'scopeSemantics'])
|
|
321
|
+
|| typeof cost.target !== 'string' || !listedNative.has(cost.target) || cost.status !== 'within_cap'
|
|
322
|
+
|| cost.metric !== 'lines' || typeof cost.value !== 'number' || !Number.isSafeInteger(cost.value) || cost.value < 0
|
|
323
|
+
|| typeof cost.conditionalValue !== 'number' || !Number.isSafeInteger(cost.conditionalValue) || cost.conditionalValue < 0
|
|
324
|
+
|| typeof cost.cap !== 'number' || !Number.isSafeInteger(cost.cap) || cost.cap < cost.value || typeof cost.measuredOn !== 'string' || !DATE.test(cost.measuredOn)
|
|
325
|
+
|| typeof cost.provenance !== 'string' || cost.provenance.length === 0
|
|
326
|
+
|| cost.scopeSemantics !== 'conditional' && cost.scopeSemantics !== 'full')
|
|
327
|
+
|| new Set(tierGuide.costs.map(cost => object(cost) ? cost.target : undefined)).size !== listedNative.size)) {
|
|
328
|
+
return refuse('SURFACE_INPUT_INVALID', 'testing tier guide lost its bounded load-cost proof');
|
|
329
|
+
}
|
|
330
|
+
if (captured.instructions !== null && (typeof captured.instructions !== 'string' || captured.instructions.length === 0
|
|
331
|
+
|| Buffer.byteLength(captured.instructions) > 65_536)) {
|
|
332
|
+
return refuse('SURFACE_INPUT_INVALID', 'consumer instructions must be absent or a bounded non-empty string');
|
|
333
|
+
}
|
|
334
|
+
const evidence = captured.sourceEvidence.length <= 512 && captured.sourceEvidence.every(validEvidence)
|
|
335
|
+
&& new Set(captured.sourceEvidence.map(row => JSON.stringify(row))).size === captured.sourceEvidence.length
|
|
336
|
+
? captured.sourceEvidence : undefined;
|
|
337
|
+
if (!evidence)
|
|
338
|
+
return refuse('SURFACE_INPUT_INVALID', 'guide and MCP evidence must be authenticated bounded data');
|
|
339
|
+
for (const guide of captured.projection.guides) {
|
|
340
|
+
if (!object(guide) || typeof guide.sourceGuidePath !== 'string' || typeof guide.sourceContentSha256 !== 'string'
|
|
341
|
+
|| typeof guide.sourceContentIdentity !== 'string' || typeof guide.bytes !== 'string' || guide.bytes.length === 0
|
|
342
|
+
|| !evidence.some(row => row.kind === 'guide' && row.path === guide.sourceGuidePath
|
|
343
|
+
&& row.sha256 === guide.sourceContentSha256 && row.identity === guide.sourceContentIdentity)) {
|
|
344
|
+
return refuse('SURFACE_INPUT_INVALID', 'projection guide output is not bound to matching evidence');
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
for (const server of captured.mcp.result.servers) {
|
|
348
|
+
if (!object(server) || !Array.isArray(server.sources))
|
|
349
|
+
return refuse('SURFACE_INPUT_INVALID', 'MCP result is malformed');
|
|
350
|
+
for (const source of server.sources)
|
|
351
|
+
if (!object(source) || typeof source.path !== 'string' || typeof source.sha256 !== 'string'
|
|
352
|
+
|| !evidence.some(row => row.kind === 'mcp' && row.path === source.path && row.sha256 === source.sha256)) {
|
|
353
|
+
return refuse('SURFACE_INPUT_INVALID', 'MCP output is not bound to matching source evidence');
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
const mcpTargets = new Set();
|
|
357
|
+
const mcpHarnesses = new Set();
|
|
358
|
+
for (const harness of captured.mcp.result.harnesses) {
|
|
359
|
+
if (!object(harness) || typeof harness.target !== 'string' || !BUILT_INS.has(harness.target)
|
|
360
|
+
|| !configuredSet.has(harness.target) || !Array.isArray(harness.servers)
|
|
361
|
+
|| mcpHarnesses.has(harness.target))
|
|
362
|
+
return refuse('SURFACE_INPUT_INVALID', 'MCP harness output is malformed');
|
|
363
|
+
mcpHarnesses.add(harness.target);
|
|
364
|
+
if (harness.servers.length > 0)
|
|
365
|
+
mcpTargets.add(harness.target);
|
|
366
|
+
}
|
|
367
|
+
// A remote server may fit none of the listed tools, and says so; a local one fits every tool.
|
|
368
|
+
if (captured.mcp.result.servers.some(server => object(server) && server.transport !== 'http') && mcpTargets.size === 0) {
|
|
369
|
+
return refuse('SURFACE_INPUT_INVALID', 'MCP model has no matching native transport');
|
|
370
|
+
}
|
|
371
|
+
const mcpModelDigest = surfaceModelSha256(captured.mcp.result);
|
|
372
|
+
if (!mcpModelDigest)
|
|
373
|
+
return refuse('SURFACE_BOUNDS', 'MCP model cannot be captured for native binding');
|
|
374
|
+
const mcpNative = captured.mcp.result.servers.length === 0 && object(captured.nativeMcp)
|
|
375
|
+
&& captured.nativeMcp.modelSha256 === mcpModelDigest && Array.isArray(captured.nativeMcp.fragments) && captured.nativeMcp.fragments.length === 0
|
|
376
|
+
? [] : nativeFragments(captured.nativeMcp, mcpModelDigest, mcpTargets);
|
|
377
|
+
if (!mcpNative)
|
|
378
|
+
return refuse('SURFACE_INPUT_INVALID', 'nonempty MCP model requires exact native fragments bound to the same approvals');
|
|
379
|
+
const grantTargets = new Set();
|
|
380
|
+
for (const grant of captured.projection.grants)
|
|
381
|
+
if (object(grant) && typeof grant.target === 'string')
|
|
382
|
+
grantTargets.add(grant.target);
|
|
383
|
+
const grantsDigest = surfaceModelSha256(captured.projection.grants);
|
|
384
|
+
if (!grantsDigest)
|
|
385
|
+
return refuse('SURFACE_BOUNDS', 'directory grants cannot be captured for native binding');
|
|
386
|
+
const grantNative = captured.projection.grants.length === 0 && object(captured.nativeGrants)
|
|
387
|
+
&& captured.nativeGrants.modelSha256 === grantsDigest && Array.isArray(captured.nativeGrants.fragments) && captured.nativeGrants.fragments.length === 0
|
|
388
|
+
? [] : nativeFragments(captured.nativeGrants, grantsDigest, grantTargets);
|
|
389
|
+
if (!grantNative)
|
|
390
|
+
return refuse('SURFACE_INPUT_INVALID', 'directory grants require exact native fragments bound to the same projection');
|
|
391
|
+
const whole = new Map();
|
|
392
|
+
const markdown = new Map();
|
|
393
|
+
const native = new Map();
|
|
394
|
+
const browserMcp = new Map();
|
|
395
|
+
const readonly = new Map();
|
|
396
|
+
const add = (map, path, contribution) => map.set(path, [...map.get(path) ?? [], contribution]);
|
|
397
|
+
for (const target of BUILT_INS)
|
|
398
|
+
if (configuredSet.has(target))
|
|
399
|
+
for (const reference of skillReferences) {
|
|
400
|
+
add(whole, `${harnessFact(target).outputs.skills}/${reference.skill}/${reference.path}`, {
|
|
401
|
+
harnesses: [target], scope: 'always', capability: 'instructed', bytes: stripDevelopmentMarkers(reference.bytes),
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
for (const candidate of captured.generated.files) {
|
|
405
|
+
if (!object(candidate) || !exactKeys(candidate, ['target', 'path', 'bytes', 'sha256']) || typeof candidate.target !== 'string'
|
|
406
|
+
|| !configuredSet.has(candidate.target) || !validFileFields(candidate))
|
|
407
|
+
return refuse('SURFACE_INPUT_INVALID', 'generated surface output is malformed');
|
|
408
|
+
const contribution = { harnesses: [candidate.target], scope: 'always', capability: 'instructed', bytes: stripDevelopmentMarkers(candidate.bytes) };
|
|
409
|
+
if (candidate.path !== 'CLAUDE.md') {
|
|
410
|
+
add(MARKDOWN_PATHS.has(candidate.path) ? markdown : whole, candidate.path, contribution);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
if (browser && Array.isArray(browser.files))
|
|
414
|
+
for (const candidate of browser.files) {
|
|
415
|
+
if (!object(candidate) || !exactKeys(candidate, ['target', 'path', 'bytes', 'sha256']) || typeof candidate.target !== 'string'
|
|
416
|
+
|| !NATIVE_BUILT_INS.has(candidate.target) || !configuredSet.has(candidate.target) || !validFileFields(candidate)) {
|
|
417
|
+
return refuse('SURFACE_INPUT_INVALID', 'browser surface output is malformed');
|
|
418
|
+
}
|
|
419
|
+
const path = candidate.path;
|
|
420
|
+
const contribution = { harnesses: [candidate.target], scope: 'always',
|
|
421
|
+
capability: 'instructed', bytes: stripDevelopmentMarkers(candidate.bytes) };
|
|
422
|
+
if (path.endsWith('/skills/cockpit-browser/SKILL.md')) {
|
|
423
|
+
add(whole, path, contribution);
|
|
424
|
+
add(whole, `${path.slice(0, -'SKILL.md'.length)}references/browser.md`, { ...contribution, bytes: stripDevelopmentMarkers(captured.browserReference) });
|
|
425
|
+
}
|
|
426
|
+
else if (['.mcp.json', '.codex/config.toml', 'opencode.jsonc'].includes(path)) {
|
|
427
|
+
if (browserMcp.has(path))
|
|
428
|
+
return refuse('SURFACE_PATH_CONFLICT', 'browser emitted duplicate native MCP output', path);
|
|
429
|
+
browserMcp.set(path, contribution);
|
|
430
|
+
}
|
|
431
|
+
else
|
|
432
|
+
return refuse('SURFACE_INPUT_INVALID', 'browser emitted an unsupported physical path', path);
|
|
433
|
+
}
|
|
434
|
+
const instructionPaths = SURFACE_TARGETS.filter(target => configuredSet.has(target)).map(target => [target, 'AGENTS.md']);
|
|
435
|
+
if (configuredSet.has('claudecode'))
|
|
436
|
+
add(markdown, 'CLAUDE.md', { harnesses: ['claudecode'], scope: 'always', capability: 'instructed', bytes: '@AGENTS.md' });
|
|
437
|
+
if (typeof captured.instructions === 'string')
|
|
438
|
+
for (const [target, path] of instructionPaths) {
|
|
439
|
+
add(markdown, path, { harnesses: [target], scope: 'always', capability: 'instructed', bytes: captured.instructions });
|
|
440
|
+
}
|
|
441
|
+
if (testing && tierGuide && nativeHooks) {
|
|
442
|
+
for (const [target, path] of instructionPaths) {
|
|
443
|
+
add(markdown, path, { harnesses: [target], scope: 'always', capability: 'instructed', bytes: stripDevelopmentMarkers(tierGuide.bytes) });
|
|
444
|
+
}
|
|
445
|
+
const hooks = nativeHooks;
|
|
446
|
+
if (hooks.kind === 'complete') {
|
|
447
|
+
if (!exactKeys(hooks, ['kind', 'entries', 'cost', 'activation']) || !Array.isArray(hooks.entries) || hooks.entries.length !== 1) {
|
|
448
|
+
return refuse('SURFACE_INPUT_INVALID', 'native hook projection is malformed');
|
|
449
|
+
}
|
|
450
|
+
if (!object(hooks.cost) || !exactKeys(hooks.cost, ['kind', 'scope', 'executionSha256', 'limitMs', 'measuredMs'])
|
|
451
|
+
|| hooks.cost.kind !== 'within-budget' || hooks.cost.scope !== 'supplied-execution-environment'
|
|
452
|
+
|| typeof hooks.cost.executionSha256 !== 'string' || !SHA256.test(hooks.cost.executionSha256)
|
|
453
|
+
|| typeof hooks.cost.limitMs !== 'number' || !Number.isSafeInteger(hooks.cost.limitMs) || hooks.cost.limitMs < 1
|
|
454
|
+
|| typeof hooks.cost.measuredMs !== 'number' || !Number.isSafeInteger(hooks.cost.measuredMs) || hooks.cost.measuredMs < 0 || hooks.cost.measuredMs > hooks.cost.limitMs
|
|
455
|
+
|| !object(hooks.activation) || !exactKeys(hooks.activation, ['kind', 'hooksPath', 'mode'])
|
|
456
|
+
|| hooks.activation.kind !== 'explicit-native-install' || hooks.activation.hooksPath !== '.githooks'
|
|
457
|
+
|| hooks.activation.mode !== 493)
|
|
458
|
+
return refuse('SURFACE_INPUT_INVALID', 'native hook projection is malformed');
|
|
459
|
+
const hook = hooks.entries[0];
|
|
460
|
+
if (!object(hook) || !exactKeys(hook, ['path', 'kind', 'ownership', 'harnesses', 'writer', 'schemaVersion', 'scope', 'capability', 'bytes', 'sha256'])
|
|
461
|
+
|| hook.path !== '.githooks/pre-push' || hook.kind !== 'file' || hook.ownership !== 'whole-file'
|
|
462
|
+
|| hook.writer !== 'cockpit' || hook.schemaVersion !== 1 || hook.scope !== 'scoped' || hook.capability !== 'instructed'
|
|
463
|
+
|| !Array.isArray(hook.harnesses) || hook.harnesses.length === 0 || hook.harnesses.some(value => typeof value !== 'string' || !configuredSet.has(value))
|
|
464
|
+
|| !validFileFields(hook))
|
|
465
|
+
return refuse('SURFACE_INPUT_INVALID', 'native hook projection is malformed');
|
|
466
|
+
add(whole, hook.path, { harnesses: hook.harnesses, scope: 'scoped', capability: 'instructed', bytes: hook.bytes });
|
|
467
|
+
}
|
|
468
|
+
else if ((hooks.kind !== 'not-installed' && hooks.kind !== 'ci-required' && hooks.kind !== 'refusal')
|
|
469
|
+
|| !exactKeys(hooks, ['kind', 'reason']) || typeof hooks.reason !== 'string' || hooks.reason.length === 0
|
|
470
|
+
|| Buffer.byteLength(hooks.reason) > 4_096)
|
|
471
|
+
return refuse('SURFACE_INPUT_INVALID', 'native hook projection is malformed');
|
|
472
|
+
else if (hooks.kind !== 'not-installed')
|
|
473
|
+
return refuse('SURFACE_INPUT_INVALID', hooks.reason, '.githooks/pre-push');
|
|
474
|
+
}
|
|
475
|
+
for (const fragment of [...mcpNative, ...grantNative])
|
|
476
|
+
native.set(fragment.path, [...native.get(fragment.path) ?? [], fragment]);
|
|
477
|
+
for (const wrapper of captured.mcp.result.wrappers) {
|
|
478
|
+
if (!validFile(wrapper))
|
|
479
|
+
return refuse('SURFACE_INPUT_INVALID', 'MCP wrapper output is malformed');
|
|
480
|
+
// A local server's wrapper starts it; a remote server's wrapper runs its headers helper.
|
|
481
|
+
const server = captured.mcp.result.servers.find(row => object(row) && typeof row.name === 'string'
|
|
482
|
+
&& wrapper.path === `.cockpit/mcp/${sha256Hex(row.name)}${row.transport === 'http' ? '.headers' : ''}.sh`);
|
|
483
|
+
const harnesses = object(server) ? captured.mcp.result.harnesses.filter(row => object(row)
|
|
484
|
+
&& Array.isArray(row.servers) && row.servers.some(transport => object(transport) && transport.name === server.name))
|
|
485
|
+
.map(row => row.target) : [];
|
|
486
|
+
if (harnesses.length === 0)
|
|
487
|
+
return refuse('SURFACE_INPUT_INVALID', 'MCP wrapper has no matching native transport');
|
|
488
|
+
add(whole, wrapper.path, { harnesses, scope: 'always', capability: 'instructed', bytes: wrapper.bytes });
|
|
489
|
+
}
|
|
490
|
+
// Every tool reads the root AGENTS.md, so the copied sibling guides follow cockpit's own instructions there.
|
|
491
|
+
for (const guide of captured.projection.guides)
|
|
492
|
+
for (const [target, path] of instructionPaths) {
|
|
493
|
+
add(markdown, path, { harnesses: [target], scope: 'always', capability: 'instructed', bytes: guide.bytes });
|
|
494
|
+
}
|
|
495
|
+
const restrictedHarnesses = captured.restrictions.targets.filter(row => object(row) && row.capability === 'instructed')
|
|
496
|
+
.map(row => row.target);
|
|
497
|
+
if (restrictedHarnesses.length > 0)
|
|
498
|
+
add(whole, captured.restrictions.sharedGuard.path, {
|
|
499
|
+
harnesses: restrictedHarnesses, scope: 'always', capability: 'instructed', bytes: captured.restrictions.sharedGuard.bytes,
|
|
500
|
+
});
|
|
501
|
+
for (const target of captured.restrictions.targets) {
|
|
502
|
+
if (!object(target) || typeof target.target !== 'string' || !configuredSet.has(target.target) || !Array.isArray(target.files) || !object(target.readonly)) {
|
|
503
|
+
return refuse('SURFACE_INPUT_INVALID', 'restriction output is malformed');
|
|
504
|
+
}
|
|
505
|
+
if (target.capability === 'unavailable') {
|
|
506
|
+
if (target.files.length !== 0 || target.readonly.fragment !== null)
|
|
507
|
+
return refuse('SURFACE_INPUT_INVALID', 'unavailable restriction target emitted output');
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
if (target.capability !== 'instructed')
|
|
511
|
+
return refuse('SURFACE_INPUT_INVALID', 'restriction capability was promoted');
|
|
512
|
+
for (const file of target.files) {
|
|
513
|
+
if (!validFile(file))
|
|
514
|
+
return refuse('SURFACE_INPUT_INVALID', 'restriction file is malformed');
|
|
515
|
+
add(whole, file.path, { harnesses: [target.target], scope: 'always', capability: 'instructed', bytes: file.bytes });
|
|
516
|
+
}
|
|
517
|
+
if (target.readonly.fragment !== null) {
|
|
518
|
+
const fragment = target.readonly.fragment;
|
|
519
|
+
if (!validFile(fragment))
|
|
520
|
+
return refuse('SURFACE_INPUT_INVALID', 'readonly fragment is malformed');
|
|
521
|
+
if (readonly.has(fragment.path))
|
|
522
|
+
return refuse('SURFACE_PATH_CONFLICT', 'readonly fragment conflicts', fragment.path);
|
|
523
|
+
readonly.set(fragment.path, fragment.bytes);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
for (const { receipt, file } of custom.contributions)
|
|
527
|
+
add(MARKDOWN_PATHS.has(file.path) ? markdown : whole, file.path, {
|
|
528
|
+
harnesses: [receipt.harness], scope: file.scope, capability: receipt.state, bytes: file.bytes,
|
|
529
|
+
});
|
|
530
|
+
for (const [path, fragments] of native) {
|
|
531
|
+
const finalBytes = new Set(fragments.map(fragment => fragment.bytes));
|
|
532
|
+
if (finalBytes.size !== 1)
|
|
533
|
+
return refuse('SURFACE_PATH_CONFLICT', 'native fragments for one physical path diverge', path);
|
|
534
|
+
const contributions = whole.get(path) ?? [];
|
|
535
|
+
const replaced = new Set();
|
|
536
|
+
for (const fragment of fragments) {
|
|
537
|
+
const baseRow = captured.generated.files.find(row => object(row) && row.target === fragment.target && row.path === path);
|
|
538
|
+
if (baseRow === undefined) {
|
|
539
|
+
if (fragment.baseSha256 !== sha256Hex(''))
|
|
540
|
+
return refuse('SURFACE_INPUT_INVALID', 'native fragment is not bound to an absent base', path);
|
|
541
|
+
}
|
|
542
|
+
else if (!object(baseRow) || baseRow.sha256 !== fragment.baseSha256) {
|
|
543
|
+
return refuse('SURFACE_INPUT_INVALID', 'native fragment base digest does not match generated output', path);
|
|
544
|
+
}
|
|
545
|
+
else
|
|
546
|
+
replaced.add(`${fragment.target}\0${baseRow.bytes}`);
|
|
547
|
+
}
|
|
548
|
+
const retained = contributions.filter(row => !row.harnesses.some(harness => replaced.has(`${harness}\0${row.bytes}`)));
|
|
549
|
+
whole.set(path, [...retained, { harnesses: fragments.map(fragment => fragment.target), scope: 'always', capability: 'instructed', bytes: fragments[0].bytes }]);
|
|
550
|
+
}
|
|
551
|
+
for (const [path, contribution] of browserMcp) {
|
|
552
|
+
const bases = whole.get(path);
|
|
553
|
+
if (!bases || bases.length !== 1)
|
|
554
|
+
return refuse('SURFACE_PATH_CONFLICT', 'browser MCP requires one existing native configuration', path);
|
|
555
|
+
const bytes = mergeBrowserMcp(path, bases[0].bytes, contribution.bytes);
|
|
556
|
+
if (!bytes)
|
|
557
|
+
return refuse('SURFACE_PATH_CONFLICT', 'browser MCP conflicts with an existing server', path);
|
|
558
|
+
whole.set(path, [{ harnesses: [...bases[0].harnesses, ...contribution.harnesses], scope: 'always',
|
|
559
|
+
capability: weakest([bases[0].capability, contribution.capability]), bytes }]);
|
|
560
|
+
}
|
|
561
|
+
for (const [path, fragment] of readonly) {
|
|
562
|
+
const bases = whole.get(path);
|
|
563
|
+
if (!bases || bases.length !== 1)
|
|
564
|
+
return refuse('SURFACE_PATH_CONFLICT', 'readonly fragment requires one generated agent file', path);
|
|
565
|
+
const combined = frontmatter(bases[0].bytes, fragment);
|
|
566
|
+
if (combined === undefined)
|
|
567
|
+
return refuse('SURFACE_PATH_CONFLICT', 'readonly frontmatter conflicts with the generated agent file', path);
|
|
568
|
+
whole.set(path, [{ ...bases[0], bytes: combined }]);
|
|
569
|
+
}
|
|
570
|
+
const entries = [];
|
|
571
|
+
for (const [path, contributions] of whole) {
|
|
572
|
+
if (path === 'opencode.jsonc')
|
|
573
|
+
continue;
|
|
574
|
+
const distinct = [...new Set(contributions.map(row => row.bytes))];
|
|
575
|
+
let bytes;
|
|
576
|
+
if (distinct.length === 1)
|
|
577
|
+
bytes = distinct[0];
|
|
578
|
+
else if (path === '.claude/settings.json' || path === '.codex/hooks.json')
|
|
579
|
+
bytes = hookJson(contributions);
|
|
580
|
+
if (bytes === undefined)
|
|
581
|
+
return refuse('SURFACE_PATH_CONFLICT', 'producers emitted divergent bytes without a purpose-built composition rule', path);
|
|
582
|
+
const harnesses = sorted(contributions.flatMap(row => row.harnesses));
|
|
583
|
+
if (!harnesses)
|
|
584
|
+
return refuse('SURFACE_INPUT_INVALID', 'physical output has no configured harness', path);
|
|
585
|
+
const scope = sharedScope(contributions);
|
|
586
|
+
if (!scope)
|
|
587
|
+
return refuse('SURFACE_PATH_CONFLICT', 'producers emitted inconsistent scopes for one physical path', path);
|
|
588
|
+
entries.push({ kind: 'file', path, harnesses, scope, capability: weakest(contributions.map(row => row.capability)), writer: 'cockpit', schemaVersion: 1,
|
|
589
|
+
ownership: 'whole-file', bytes, sha256: sha256Hex(bytes) });
|
|
590
|
+
}
|
|
591
|
+
for (const [path, contributions] of markdown) {
|
|
592
|
+
const generatorRows = path === 'CLAUDE.md' ? [] : captured.generated.files.filter(row => object(row) && row.path === path);
|
|
593
|
+
if (generatorRows.length > 1 && new Set(generatorRows.map(row => object(row) ? row.bytes : undefined)).size !== 1) {
|
|
594
|
+
return refuse('SURFACE_PATH_CONFLICT', 'shared generator instruction bases diverge', path);
|
|
595
|
+
}
|
|
596
|
+
const base = generatorRows.length > 0 && object(generatorRows[0]) ? stripDevelopmentMarkers(generatorRows[0].bytes) : '';
|
|
597
|
+
const additions = contributions.filter(row => row.bytes !== base).map(row => row.bytes);
|
|
598
|
+
const body = [base.replace(/\n+$/, ''), ...new Set(additions)].filter(Boolean).join('\n') + '\n';
|
|
599
|
+
const harnesses = sorted(contributions.flatMap(row => row.harnesses));
|
|
600
|
+
if (!harnesses)
|
|
601
|
+
return refuse('SURFACE_INPUT_INVALID', 'shared Markdown output has no configured harness', path);
|
|
602
|
+
const scope = sharedScope(contributions);
|
|
603
|
+
if (!scope)
|
|
604
|
+
return refuse('SURFACE_PATH_CONFLICT', 'producers emitted inconsistent scopes for one physical path', path);
|
|
605
|
+
entries.push({ kind: 'shared-region', path, harnesses, scope, capability: weakest(contributions.map(row => row.capability)), writer: 'cockpit', schemaVersion: 1,
|
|
606
|
+
ownership: 'shared-region', regionId: 'cockpit-instructions', region: { format: 'markdown-delimited',
|
|
607
|
+
begin: '<!-- cockpit:instructions:start -->', end: '<!-- cockpit:instructions:end -->', bytes: body, sha256: sha256Hex(body) } });
|
|
608
|
+
}
|
|
609
|
+
const openCode = whole.get('opencode.jsonc') ?? [];
|
|
610
|
+
if (openCode.length > 0) {
|
|
611
|
+
const distinct = [...new Set(openCode.map(row => row.bytes))];
|
|
612
|
+
if (distinct.length !== 1)
|
|
613
|
+
return refuse('SURFACE_PATH_CONFLICT', 'OpenCode native producers emitted divergent base bytes', 'opencode.jsonc');
|
|
614
|
+
const members = captureOpenCodeRootMembers(distinct[0]);
|
|
615
|
+
if (!members)
|
|
616
|
+
return refuse('SURFACE_OWNERSHIP_INVALID', 'OpenCode base must be strict JSON containing only admitted native root keys', 'opencode.jsonc');
|
|
617
|
+
const grouped = [...members].sort((left, right) => left.key < right.key ? -1 : left.key > right.key ? 1 : 0);
|
|
618
|
+
const contributions = openCode;
|
|
619
|
+
const harnesses = sorted(contributions.flatMap(row => row.harnesses));
|
|
620
|
+
if (!harnesses)
|
|
621
|
+
return refuse('SURFACE_INPUT_INVALID', 'OpenCode output has no configured harness');
|
|
622
|
+
const scope = sharedScope(contributions);
|
|
623
|
+
if (!scope)
|
|
624
|
+
return refuse('SURFACE_PATH_CONFLICT', 'producers emitted inconsistent scopes for one physical path', 'opencode.jsonc');
|
|
625
|
+
entries.push({ kind: 'shared-region', path: 'opencode.jsonc', harnesses, scope, capability: weakest(contributions.map(row => row.capability)), writer: 'cockpit', schemaVersion: 1,
|
|
626
|
+
ownership: 'shared-region', regionId: 'cockpit-opencode-root-properties',
|
|
627
|
+
region: { format: 'opencode-json-root-properties', members: grouped, sha256: sha256Hex(JSON.stringify(grouped)) } });
|
|
628
|
+
}
|
|
629
|
+
entries.sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0);
|
|
630
|
+
const ownership = classifySurfaceOwnership({ configuredHarnesses: configured, entries });
|
|
631
|
+
if (ownership.kind === 'refusal')
|
|
632
|
+
return ownership;
|
|
633
|
+
const result = snapshotGeneratedJson({ kind: 'complete', entries: ownership.entries, warnings,
|
|
634
|
+
inputs: [...evidence, ...custom.evidence].sort((left, right) => JSON.stringify(left) < JSON.stringify(right) ? -1 : JSON.stringify(left) > JSON.stringify(right) ? 1 : 0) });
|
|
635
|
+
return result ? result : refuse('SURFACE_BOUNDS', 'surface render result exceeds the captured bounds');
|
|
636
|
+
}
|