@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,129 @@
|
|
|
1
|
+
// Owns the machine-local guard identity file: its path, its bytes and how `sync` writes it.
|
|
2
|
+
// Tracked guard bytes must be identical on every clone, so everything that names this machine
|
|
3
|
+
// (Node, the installed cockpit module, root and configuration device and inode numbers) lives here,
|
|
4
|
+
// in a directory that ignores itself. It decides nothing: the guard application authenticates the
|
|
5
|
+
// file against the running process and the published surfaces before any policy is resolved.
|
|
6
|
+
import { exactKeys } from '../exact-keys.js';
|
|
7
|
+
import { randomBytes } from 'node:crypto';
|
|
8
|
+
import { lstat, mkdir, rename, rm, writeFile } from 'node:fs/promises';
|
|
9
|
+
import { basename, join } from 'node:path';
|
|
10
|
+
import { readBoundedInput } from '../config/load.js';
|
|
11
|
+
import { errorCause, hasErrorCode } from '../environment/contract.js';
|
|
12
|
+
export const GUARD_LOCAL_PATHS = Object.freeze({
|
|
13
|
+
directory: '.cockpit/local',
|
|
14
|
+
identity: '.cockpit/local/guard-identity',
|
|
15
|
+
// The exact configuration the guards were generated from, so a later edit can be compared with it.
|
|
16
|
+
generatedConfig: '.cockpit/local/guard-config',
|
|
17
|
+
ignore: '.cockpit/local/.gitignore',
|
|
18
|
+
});
|
|
19
|
+
// The guard script reads this file with `read` one line at a time, so the layout is lines rather
|
|
20
|
+
// than JSON: a format line, the two executables it launches, then the identity the application checks.
|
|
21
|
+
const FORMAT = 'cockpit-guard-local/v1';
|
|
22
|
+
const LOCAL_FILE_MAX_BYTES = 65_536;
|
|
23
|
+
const KEYS = ['root', 'rootDevice', 'rootInode', 'configPath', 'configSha256', 'configDevice', 'configInode',
|
|
24
|
+
'node', 'nodeSha256', 'nodeFingerprint', 'module', 'moduleSha256'];
|
|
25
|
+
const IGNORE_BYTES = '# cockpit-owned: machine-local files, written by `cockpit sync`, never committed\n*\n';
|
|
26
|
+
export function encodeGuardLocalIdentity(identity) {
|
|
27
|
+
const ordered = Object.fromEntries(KEYS.map(key => [key, identity[key]]));
|
|
28
|
+
return `${FORMAT}\n${identity.node}\n${identity.module}\n${JSON.stringify(ordered)}\n`;
|
|
29
|
+
}
|
|
30
|
+
/** The exact identity the bytes carry, or undefined when they are not a complete current-format file. */
|
|
31
|
+
export function parseGuardLocalIdentity(bytes) {
|
|
32
|
+
const lines = bytes.split('\n');
|
|
33
|
+
if (lines.length !== 5 || lines[0] !== FORMAT || lines[4] !== '')
|
|
34
|
+
return undefined;
|
|
35
|
+
let value;
|
|
36
|
+
try {
|
|
37
|
+
value = JSON.parse(lines[3]);
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
if (error instanceof SyntaxError)
|
|
41
|
+
return undefined; // not the current format
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value))
|
|
45
|
+
return undefined;
|
|
46
|
+
const record = value;
|
|
47
|
+
if (!exactKeys(record, KEYS) || !KEYS.every(key => typeof record[key] === 'string'))
|
|
48
|
+
return undefined;
|
|
49
|
+
const identity = record;
|
|
50
|
+
return identity.node === lines[1] && identity.module === lines[2] && encodeGuardLocalIdentity(identity) === bytes ? identity : undefined;
|
|
51
|
+
}
|
|
52
|
+
async function observeLocalFile(path) {
|
|
53
|
+
try {
|
|
54
|
+
const before = await lstat(path);
|
|
55
|
+
if (!before.isFile() || before.isSymbolicLink() || before.size > LOCAL_FILE_MAX_BYTES)
|
|
56
|
+
return { kind: 'invalid' };
|
|
57
|
+
const read = await readBoundedInput(path);
|
|
58
|
+
if (read.kind !== 'complete' || read.bytes.length > LOCAL_FILE_MAX_BYTES)
|
|
59
|
+
return { kind: 'invalid' };
|
|
60
|
+
const after = await lstat(path);
|
|
61
|
+
if (!after.isFile() || after.isSymbolicLink() || after.dev !== before.dev || after.ino !== before.ino
|
|
62
|
+
|| after.size !== before.size || after.mtimeMs !== before.mtimeMs || after.ctimeMs !== before.ctimeMs)
|
|
63
|
+
return { kind: 'invalid' };
|
|
64
|
+
return { kind: 'complete', status: after, bytes: read.bytes };
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
return hasErrorCode(error, 'ENOENT') ? { kind: 'absent' } : { kind: 'invalid' };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
async function directoryAt(path, create) {
|
|
71
|
+
try {
|
|
72
|
+
const status = await lstat(path);
|
|
73
|
+
return status.isDirectory() && !status.isSymbolicLink() ? undefined : `${path} is not a plain directory`;
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
if (!hasErrorCode(error, 'ENOENT') || !create)
|
|
77
|
+
return `${path} is unavailable (${error.code ?? 'unknown error'})`;
|
|
78
|
+
}
|
|
79
|
+
await mkdir(path, { mode: 0o700 });
|
|
80
|
+
return directoryAt(path, false);
|
|
81
|
+
}
|
|
82
|
+
// Rename replaces a planted symlink at the destination instead of writing through it.
|
|
83
|
+
async function replaceFile(directory, name, bytes) {
|
|
84
|
+
const temporary = join(directory, `.${name}.${randomBytes(8).toString('hex')}.tmp`);
|
|
85
|
+
try {
|
|
86
|
+
await writeFile(temporary, bytes, { flag: 'wx', mode: 0o600 });
|
|
87
|
+
await rename(temporary, join(directory, name));
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
await rm(temporary, { force: true });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Publish the identity after the tracked surfaces it authenticates, with the configuration text it
|
|
95
|
+
* names written first; the reason names what failed.
|
|
96
|
+
*/
|
|
97
|
+
export async function writeGuardLocalIdentity(root, identity, configText) {
|
|
98
|
+
return writeCockpitLocalFiles(root, [{ name: basename(GUARD_LOCAL_PATHS.generatedConfig), bytes: configText },
|
|
99
|
+
{ name: basename(GUARD_LOCAL_PATHS.identity), bytes: encodeGuardLocalIdentity(identity) }]);
|
|
100
|
+
}
|
|
101
|
+
/** Replace machine-local files under `.cockpit/local`, a directory that ignores itself; the reason names what failed. */
|
|
102
|
+
export async function writeCockpitLocalFiles(root, files) {
|
|
103
|
+
let current = GUARD_LOCAL_PATHS.directory;
|
|
104
|
+
try {
|
|
105
|
+
const parent = await directoryAt(join(root, '.cockpit'), false);
|
|
106
|
+
if (parent)
|
|
107
|
+
return { kind: 'refusal', message: parent };
|
|
108
|
+
const directory = join(root, GUARD_LOCAL_PATHS.directory);
|
|
109
|
+
const local = await directoryAt(directory, true);
|
|
110
|
+
if (local)
|
|
111
|
+
return { kind: 'refusal', message: local };
|
|
112
|
+
current = GUARD_LOCAL_PATHS.ignore;
|
|
113
|
+
const ignore = join(root, GUARD_LOCAL_PATHS.ignore);
|
|
114
|
+
const currentIgnore = await observeLocalFile(ignore);
|
|
115
|
+
if (currentIgnore.kind !== 'complete' || currentIgnore.bytes.toString('utf8') !== IGNORE_BYTES) {
|
|
116
|
+
await replaceFile(directory, '.gitignore', IGNORE_BYTES);
|
|
117
|
+
}
|
|
118
|
+
for (const file of files) {
|
|
119
|
+
current = `${GUARD_LOCAL_PATHS.directory}/${file.name}`;
|
|
120
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(file.name))
|
|
121
|
+
return { kind: 'refusal', message: `${current} is not a plain local file name` };
|
|
122
|
+
await replaceFile(directory, file.name, file.bytes);
|
|
123
|
+
}
|
|
124
|
+
return { kind: 'complete' };
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
return { kind: 'refusal', message: `${current} could not be written (${errorCause(error)})` };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { EnvironmentDiagnostic } from '../environment/contract.js';
|
|
2
|
+
import type { CockpitConfig } from '../schema.js';
|
|
3
|
+
type PolicyChange = Readonly<{
|
|
4
|
+
kind: 'non-widening';
|
|
5
|
+
}> | Readonly<{
|
|
6
|
+
kind: 'widening';
|
|
7
|
+
fields: readonly string[];
|
|
8
|
+
}>;
|
|
9
|
+
/**
|
|
10
|
+
* Every schema path a rule names, so a test can hold each to a field the schema still has: a rule
|
|
11
|
+
* whose path was renamed away never matches, and its field silently falls back to the default.
|
|
12
|
+
* Paths under `sibling[].` name a field of a sibling row.
|
|
13
|
+
*/
|
|
14
|
+
export declare const POLICY_CHANGE_RULE_PATHS: readonly string[];
|
|
15
|
+
/**
|
|
16
|
+
* Whether `after` lets agents do anything `before` did not, over the loaded configurations. Only a
|
|
17
|
+
* difference a rule above proves safe is non-widening; every other difference widens. `confirmed`
|
|
18
|
+
* names the answers whose recorded observation matches `after`; only those may leave `unconfirmed`.
|
|
19
|
+
*/
|
|
20
|
+
export declare function policyChange(before: CockpitConfig, after: CockpitConfig, confirmed?: ReadonlySet<string>): PolicyChange;
|
|
21
|
+
type GuardConfigChange = Readonly<{
|
|
22
|
+
kind: 'unchanged' | 'unrecorded';
|
|
23
|
+
}> | PolicyChange;
|
|
24
|
+
/**
|
|
25
|
+
* How the current configuration differs from the one the guards were generated from. `recorded` is
|
|
26
|
+
* the copy sync kept of that configuration; one missing, or not the configuration `generatedSha256`
|
|
27
|
+
* names, cannot show the edit is safe. `confirmations` is the machine-local record `cockpit confirm`
|
|
28
|
+
* keeps of the observations that confirmed answers.
|
|
29
|
+
*/
|
|
30
|
+
export declare function guardConfigChange(generatedSha256: string, recorded: Uint8Array | undefined, current: Readonly<{
|
|
31
|
+
sha256: string;
|
|
32
|
+
config: CockpitConfig;
|
|
33
|
+
}>, confirmations?: Uint8Array): GuardConfigChange;
|
|
34
|
+
/** Why a changed configuration needs a person before the guard applies it, for a hold or a check. */
|
|
35
|
+
export declare function guardConfigHeldReason(change: Exclude<GuardConfigChange, Readonly<{
|
|
36
|
+
kind: 'unchanged' | 'non-widening';
|
|
37
|
+
}>>): string;
|
|
38
|
+
/** What `check` reports about an edit the guards were not regenerated for, or nothing. */
|
|
39
|
+
export declare function inspectGuardConfigChange(root: string, configPath: string, current: Readonly<{
|
|
40
|
+
sha256: string;
|
|
41
|
+
config: CockpitConfig;
|
|
42
|
+
}>): Promise<readonly EnvironmentDiagnostic[]>;
|
|
43
|
+
export {};
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// Owns whether an edit of cockpit.toml widens what agents may do. The guard holds every agent call
|
|
2
|
+
// once the configuration differs from the one it was generated from; this classification decides
|
|
3
|
+
// whether that hold needs a person (the edit widens) or only a sync an agent may run (it does not).
|
|
4
|
+
// It fails closed: a field with no rule here widens whenever it changes, so a field added to the
|
|
5
|
+
// schema later is a person's decision until someone writes down why it is not. A rule exists only
|
|
6
|
+
// for a field that cannot widen the guard's trust inputs (the commands agents run unprompted, the
|
|
7
|
+
// roots they write, the secrets they are kept from, who passes a gate) in the direction it admits.
|
|
8
|
+
// [guards], [work], [autonomy], [mcp], [testing], [browser] and every command field have none, so
|
|
9
|
+
// any change to them is a person's.
|
|
10
|
+
// The rules read the configuration rather than compare the guard policy each side would build: most
|
|
11
|
+
// of what they judge (Compose files and profiles, resettable volumes, the tools guarded, a sibling's
|
|
12
|
+
// operator gate) never reaches that policy, and building it observes the checkouts on disk.
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
15
|
+
import { UNSYNCED_ADMITTED_PROSE } from '../command-table.js';
|
|
16
|
+
import { parseCockpitConfigBytes, readBoundedInput } from '../config/load.js';
|
|
17
|
+
import { hasErrorCode } from '../environment/contract.js';
|
|
18
|
+
import { cockpitCommand } from '../fix-catalogue.js';
|
|
19
|
+
import { sha256Hex } from '../sha256.js';
|
|
20
|
+
import { ANSWER_CONFIRMATION_PATH, confirmedAnswers } from './answer-confirmation.js';
|
|
21
|
+
import { GUARD_LOCAL_PATHS, parseGuardLocalIdentity } from './local-identity.js';
|
|
22
|
+
const safe = () => [];
|
|
23
|
+
const gains = (from, to) => to.some(row => !from.some(was => isDeepStrictEqual(was, row)));
|
|
24
|
+
/** A list that may lose rows but not gain one. */
|
|
25
|
+
const onlyRemoves = (before = [], after = [], at) => gains(before, after) ? [at] : [];
|
|
26
|
+
/** A list that may gain rows but not lose one. */
|
|
27
|
+
const onlyAdds = (before = [], after = [], at) => gains(after, before) ? [at] : [];
|
|
28
|
+
function plainObject(value) {
|
|
29
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
30
|
+
}
|
|
31
|
+
/** The paths under `at` that differ in a way no rule admits; a rule is looked up by its path from the walk's root. */
|
|
32
|
+
function walk(before, after, rules, at, key = '') {
|
|
33
|
+
const rule = rules[key];
|
|
34
|
+
if (rule)
|
|
35
|
+
return [...rule(before, after, at)];
|
|
36
|
+
if (plainObject(before) && plainObject(after)) {
|
|
37
|
+
return [...new Set([...Object.keys(before), ...Object.keys(after)])].flatMap(name => walk(before[name], after[name], rules, at ? `${at}.${name}` : name, key ? `${key}.${name}` : name));
|
|
38
|
+
}
|
|
39
|
+
return isDeepStrictEqual(before, after) ? [] : [at];
|
|
40
|
+
}
|
|
41
|
+
// The fields of an existing sibling that say what it is for. Everything else about a sibling decides
|
|
42
|
+
// what the agent may write or read: its directory is a writable root, its environment files are the
|
|
43
|
+
// instances the guard protects, its guides are copied into every agent's instructions, and its derived
|
|
44
|
+
// MCP sources start processes outside the guard.
|
|
45
|
+
const SIBLING_RULES = {
|
|
46
|
+
purpose: safe,
|
|
47
|
+
host_reason: safe,
|
|
48
|
+
// Only the stale-stack warning reads these.
|
|
49
|
+
rebuild_inputs: safe,
|
|
50
|
+
// An operator gate keeps a sibling out of default runs; adding one narrows, removing one widens.
|
|
51
|
+
gate: (before, after, at) => before !== undefined && after === undefined ? [at] : [],
|
|
52
|
+
};
|
|
53
|
+
// A new sibling is a new root the agent may write, wherever its directory is, so only a removed or
|
|
54
|
+
// described one is compared field by field.
|
|
55
|
+
const siblings = (before, after, at) => [
|
|
56
|
+
...before.flatMap(row => {
|
|
57
|
+
const now = after.find(candidate => candidate.slug === row.slug);
|
|
58
|
+
// A removed sibling takes its instance protection with it, and its directory leaves the guard's view.
|
|
59
|
+
return now ? walk(row, now, SIBLING_RULES, `${at}.${row.slug}`) : [`${at}.${row.slug}`];
|
|
60
|
+
}),
|
|
61
|
+
...after.filter(row => !before.some(candidate => candidate.slug === row.slug)).map(row => `${at}.${row.slug}`),
|
|
62
|
+
];
|
|
63
|
+
// A policy row admits its build and test commands in every sibling of its toolchain.
|
|
64
|
+
const installPolicies = (before, after, at) => after.flatMap(row => {
|
|
65
|
+
const was = before.find(candidate => candidate.toolchain === row.toolchain && candidate.lockfile === row.lockfile);
|
|
66
|
+
return ['build', 'test'].filter(kind => row[kind] !== undefined && !isDeepStrictEqual(row[kind], was?.[kind]))
|
|
67
|
+
.map(kind => `${at}.${row.toolchain}:${row.lockfile}.${kind}`);
|
|
68
|
+
});
|
|
69
|
+
// Keyed by schema path from the configuration's root.
|
|
70
|
+
const CHANGE_RULES = {
|
|
71
|
+
// Removing an entry would claim a person stated an answer nobody stated; policyChange admits the
|
|
72
|
+
// removal of one an observation recorded as confirmed for exactly the new configuration.
|
|
73
|
+
unconfirmed: onlyAdds,
|
|
74
|
+
'cockpit.guide_cap': safe,
|
|
75
|
+
sibling: siblings,
|
|
76
|
+
ports: safe,
|
|
77
|
+
'environment.compose_files': onlyRemoves,
|
|
78
|
+
// A profile starts more of the Compose files' services, as a new Compose file does.
|
|
79
|
+
'environment.profiles': onlyRemoves,
|
|
80
|
+
'environment.install_policies': installPolicies,
|
|
81
|
+
// Reset deletes exactly these volumes.
|
|
82
|
+
'environment.resettable_volumes': onlyRemoves,
|
|
83
|
+
// Addresses derived from leased ports, and the floor the host is held to: neither reaches the guard.
|
|
84
|
+
'environment.entrypoint_derivatives': safe,
|
|
85
|
+
'environment.machine': safe,
|
|
86
|
+
// Removing a tool deletes its guard registration.
|
|
87
|
+
'surfaces.tools': onlyAdds,
|
|
88
|
+
// Only docs check reads living documents. The decision paths stay a person's: work orders
|
|
89
|
+
// never count changes under them as out of scope.
|
|
90
|
+
'docs.living': safe,
|
|
91
|
+
};
|
|
92
|
+
/**
|
|
93
|
+
* Every schema path a rule names, so a test can hold each to a field the schema still has: a rule
|
|
94
|
+
* whose path was renamed away never matches, and its field silently falls back to the default.
|
|
95
|
+
* Paths under `sibling[].` name a field of a sibling row.
|
|
96
|
+
*/
|
|
97
|
+
export const POLICY_CHANGE_RULE_PATHS = Object.freeze([...Object.keys(CHANGE_RULES),
|
|
98
|
+
...Object.keys(SIBLING_RULES).map(field => `sibling[].${field}`)]);
|
|
99
|
+
/**
|
|
100
|
+
* Whether `after` lets agents do anything `before` did not, over the loaded configurations. Only a
|
|
101
|
+
* difference a rule above proves safe is non-widening; every other difference widens. `confirmed`
|
|
102
|
+
* names the answers whose recorded observation matches `after`; only those may leave `unconfirmed`.
|
|
103
|
+
*/
|
|
104
|
+
export function policyChange(before, after, confirmed = new Set()) {
|
|
105
|
+
const unconfirmed = (from = [], to = [], at) => gains(to, from.filter(answer => !confirmed.has(answer))) ? [at] : [];
|
|
106
|
+
const fields = walk(before, after, { ...CHANGE_RULES, unconfirmed }, '');
|
|
107
|
+
return fields.length ? { kind: 'widening', fields: [...new Set(fields)] } : { kind: 'non-widening' };
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* How the current configuration differs from the one the guards were generated from. `recorded` is
|
|
111
|
+
* the copy sync kept of that configuration; one missing, or not the configuration `generatedSha256`
|
|
112
|
+
* names, cannot show the edit is safe. `confirmations` is the machine-local record `cockpit confirm`
|
|
113
|
+
* keeps of the observations that confirmed answers.
|
|
114
|
+
*/
|
|
115
|
+
export function guardConfigChange(generatedSha256, recorded, current, confirmations) {
|
|
116
|
+
if (current.sha256 === generatedSha256)
|
|
117
|
+
return { kind: 'unchanged' };
|
|
118
|
+
const before = recorded && sha256Hex(recorded) === generatedSha256 ? parseCockpitConfigBytes(recorded) : undefined;
|
|
119
|
+
return before ? policyChange(before, current.config, confirmedAnswers(confirmations, current.config)) : { kind: 'unrecorded' };
|
|
120
|
+
}
|
|
121
|
+
/** Why a changed configuration needs a person before the guard applies it, for a hold or a check. */
|
|
122
|
+
export function guardConfigHeldReason(change) {
|
|
123
|
+
return change.kind === 'widening'
|
|
124
|
+
? `it widens what agents may do (${change.fields.join(', ')})`
|
|
125
|
+
: `the configuration they were generated from is not recorded in ${GUARD_LOCAL_PATHS.generatedConfig}, so the edit cannot be shown not to widen what agents may do`;
|
|
126
|
+
}
|
|
127
|
+
/** What `check` reports about an edit the guards were not regenerated for, or nothing. */
|
|
128
|
+
export async function inspectGuardConfigChange(root, configPath, current) {
|
|
129
|
+
const local = async (path) => {
|
|
130
|
+
try {
|
|
131
|
+
const read = await readBoundedInput(join(root, path));
|
|
132
|
+
return read.kind === 'complete' ? read.bytes : undefined;
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
if (hasErrorCode(error, 'ENOENT'))
|
|
136
|
+
return undefined;
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
const identityBytes = await local(GUARD_LOCAL_PATHS.identity);
|
|
141
|
+
// No guard was generated on this machine, so nothing holds the agent.
|
|
142
|
+
if (!identityBytes)
|
|
143
|
+
return [];
|
|
144
|
+
const identity = parseGuardLocalIdentity(identityBytes.toString('utf8'));
|
|
145
|
+
if (!identity)
|
|
146
|
+
return [];
|
|
147
|
+
const change = guardConfigChange(identity.configSha256, await local(GUARD_LOCAL_PATHS.generatedConfig), current, await local(ANSWER_CONFIRMATION_PATH));
|
|
148
|
+
if (change.kind === 'unchanged')
|
|
149
|
+
return [];
|
|
150
|
+
if (change.kind === 'non-widening')
|
|
151
|
+
return [{ severity: 'warning', target: 'guard',
|
|
152
|
+
code: 'GUARD_CONFIG_UNSYNCED',
|
|
153
|
+
message: `cockpit.toml changed since the guards were generated without widening what agents may do; the guard holds every agent tool call except ${UNSYNCED_ADMITTED_PROSE} until it is synced`,
|
|
154
|
+
fix: `run ${cockpitCommand('sync', { config: configPath })}; an agent may run it where [autonomy] sync = "agent"` }];
|
|
155
|
+
return [{ severity: 'error', target: 'guard',
|
|
156
|
+
code: 'GUARD_CONFIG_WIDENED',
|
|
157
|
+
message: `cockpit.toml changed since the guards were generated and ${guardConfigHeldReason(change)}; the guard holds every agent tool call until a person syncs`,
|
|
158
|
+
fix: `a person reads the change and runs ${cockpitCommand('sync', { config: configPath })} in a terminal outside the agent, or the change is reverted` }];
|
|
159
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type GuardDecision } from './evaluator.js';
|
|
2
|
+
import type { GuardFileContent, ParsedInvocation } from './invocation-parser.js';
|
|
3
|
+
/**
|
|
4
|
+
* The denial for a tool call that would change the pinned policy tables, or undefined. `isConfig`
|
|
5
|
+
* says whether a lexically resolved path is the configuration, by spelling or by file identity.
|
|
6
|
+
*/
|
|
7
|
+
export declare function pinnedPolicyDenial(input: Readonly<{
|
|
8
|
+
tool: string;
|
|
9
|
+
cwd: string;
|
|
10
|
+
content?: GuardFileContent;
|
|
11
|
+
paths: readonly string[];
|
|
12
|
+
invocations: readonly ParsedInvocation[];
|
|
13
|
+
}>, current: string, isConfig: (path: string) => boolean): GuardDecision | undefined;
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// Owns the guard's pin on the autonomy policy: an agent tool call that would change cockpit.toml's
|
|
2
|
+
// [work] (with [work.gates]) or [autonomy] table, or [mcp] while a person approves MCP servers, is a
|
|
3
|
+
// hard denial. An edit, write or patch is applied to the current text and the tables compared; a
|
|
4
|
+
// shell command that names the file and is not a proven read, a Git record or cockpit's --config
|
|
5
|
+
// operand cannot be applied, so it is denied too.
|
|
6
|
+
// The decision is pure: the guard application supplies the current text and which targets are the
|
|
7
|
+
// configuration under another name.
|
|
8
|
+
import { posix } from 'node:path';
|
|
9
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
10
|
+
import { pinnedPolicyTables } from '../autonomy.js';
|
|
11
|
+
import { innerCommand } from './evaluator.js';
|
|
12
|
+
import { classifyReadOnlyInvocation, cockpitLauncherPrefix } from './read-only.js';
|
|
13
|
+
const denial = (reason) => ({ decision: 'deny', reason });
|
|
14
|
+
const PINNED = '[work], [work.gates] and [autonomy] (and [mcp] while [autonomy] mcp_approve is not agent)';
|
|
15
|
+
/** Applies one `*** Update File` section of a patch to `text`, or undefined when a hunk does not match exactly once. */
|
|
16
|
+
function applyPatchSection(text, lines) {
|
|
17
|
+
let result = text;
|
|
18
|
+
const hunks = [];
|
|
19
|
+
for (const line of lines) {
|
|
20
|
+
if (line.startsWith('@@'))
|
|
21
|
+
hunks.push([]);
|
|
22
|
+
else if (line !== '*** End of File')
|
|
23
|
+
(hunks.at(-1) ?? (hunks.push([]), hunks.at(-1))).push(line);
|
|
24
|
+
}
|
|
25
|
+
for (const hunk of hunks) {
|
|
26
|
+
const before = hunk.filter(line => !line.startsWith('+')).map(line => line.slice(1)).join('\n');
|
|
27
|
+
const after = hunk.filter(line => !line.startsWith('-')).map(line => line.slice(1)).join('\n');
|
|
28
|
+
const first = result.indexOf(before);
|
|
29
|
+
if (first < 0 || result.indexOf(before, first + 1) >= 0)
|
|
30
|
+
return undefined;
|
|
31
|
+
result = result.slice(0, first) + after + result.slice(first + before.length);
|
|
32
|
+
}
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
function proposedText(content, isConfig, current) {
|
|
36
|
+
if (content.kind === 'write')
|
|
37
|
+
return isConfig(content.path) ? content.content : 'untouched';
|
|
38
|
+
if (content.kind === 'replace') {
|
|
39
|
+
if (!isConfig(content.path))
|
|
40
|
+
return 'untouched';
|
|
41
|
+
if (!content.old)
|
|
42
|
+
return undefined;
|
|
43
|
+
const first = current.indexOf(content.old);
|
|
44
|
+
if (first < 0 || !content.all && current.indexOf(content.old, first + 1) >= 0)
|
|
45
|
+
return undefined;
|
|
46
|
+
return content.all ? current.split(content.old).join(content.new) : current.slice(0, first) + content.new + current.slice(first + content.old.length);
|
|
47
|
+
}
|
|
48
|
+
const lines = content.text.trimEnd().split('\n').slice(1, -1);
|
|
49
|
+
let section;
|
|
50
|
+
let target;
|
|
51
|
+
let touched = false;
|
|
52
|
+
let next = current;
|
|
53
|
+
const flush = () => {
|
|
54
|
+
if (!section || !target)
|
|
55
|
+
return true;
|
|
56
|
+
if (target === 'add') {
|
|
57
|
+
next = section.map(line => line.slice(1)).join('\n') + '\n';
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
const applied = applyPatchSection(next, section);
|
|
61
|
+
if (applied === undefined)
|
|
62
|
+
return false;
|
|
63
|
+
next = applied;
|
|
64
|
+
return true;
|
|
65
|
+
};
|
|
66
|
+
for (const line of lines) {
|
|
67
|
+
const header = /^\*\*\* (Add File|Update File|Delete File|Move to): (.+)$/.exec(line);
|
|
68
|
+
if (!header) {
|
|
69
|
+
section?.push(line);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (!flush())
|
|
73
|
+
return undefined;
|
|
74
|
+
section = undefined;
|
|
75
|
+
target = undefined;
|
|
76
|
+
if (!isConfig(header[2]))
|
|
77
|
+
continue;
|
|
78
|
+
touched = true;
|
|
79
|
+
// Deleting or moving the configuration is the structural denial's; either way its tables would change.
|
|
80
|
+
if (header[1] === 'Delete File' || header[1] === 'Move to')
|
|
81
|
+
return undefined;
|
|
82
|
+
section = [];
|
|
83
|
+
target = header[1] === 'Add File' ? 'add' : 'update';
|
|
84
|
+
}
|
|
85
|
+
if (!flush())
|
|
86
|
+
return undefined;
|
|
87
|
+
return touched ? next : 'untouched';
|
|
88
|
+
}
|
|
89
|
+
const GIT_RECORDING = new Set(['add', 'commit', 'diff', 'log', 'show', 'status', 'blame']);
|
|
90
|
+
// Only the `git` the command search path finds: a path spelled `git` may be any program.
|
|
91
|
+
function recordsWithoutWriting(invocation) {
|
|
92
|
+
if (invocation.executable !== 'git')
|
|
93
|
+
return false;
|
|
94
|
+
const words = invocation.argv;
|
|
95
|
+
for (let index = 0; index < words.length; index++) {
|
|
96
|
+
const word = words[index];
|
|
97
|
+
if (word === '-C' || word === '-c') {
|
|
98
|
+
index++;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (word.startsWith('-'))
|
|
102
|
+
return false;
|
|
103
|
+
return GIT_RECORDING.has(word);
|
|
104
|
+
}
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* The invocations the pin judges. A cockpit session or exec runs the words after its `--` as a
|
|
109
|
+
* command of its own, so that command is judged as one more invocation and the wrapper by the words
|
|
110
|
+
* before it. Exec's command runs in a product container, where neither a read-only spelling nor a
|
|
111
|
+
* cockpit launcher proves what the program is, so naming the configuration there is never excused.
|
|
112
|
+
*/
|
|
113
|
+
function judgedInvocations(invocations) {
|
|
114
|
+
const judged = [];
|
|
115
|
+
const pending = invocations.map(invocation => ({ ...invocation, contained: false }));
|
|
116
|
+
for (let invocation = pending.shift(); invocation; invocation = pending.shift()) {
|
|
117
|
+
const prefix = invocation.executable === null ? undefined : cockpitLauncherPrefix(invocation.executable, invocation.argv);
|
|
118
|
+
const inner = prefix === undefined ? undefined : innerCommand(invocation.argv.slice(prefix));
|
|
119
|
+
if (!inner) {
|
|
120
|
+
judged.push(invocation);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
judged.push({ ...invocation, argv: invocation.argv.slice(0, invocation.argv.length - inner.words.length - 1) });
|
|
124
|
+
pending.push({ executable: inner.words[0], argv: inner.words.slice(1), cwd: invocation.cwd, wrapped: false, redirects: [],
|
|
125
|
+
words: inner.words.map((value, index) => ({ role: index ? 'argument' : 'executable', value })),
|
|
126
|
+
contained: invocation.contained || inner.runs === 'container' });
|
|
127
|
+
}
|
|
128
|
+
return judged;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* The denial for a tool call that would change the pinned policy tables, or undefined. `isConfig`
|
|
132
|
+
* says whether a lexically resolved path is the configuration, by spelling or by file identity.
|
|
133
|
+
*/
|
|
134
|
+
export function pinnedPolicyDenial(input, current, isConfig) {
|
|
135
|
+
const resolved = (cwd) => (path) => isConfig(posix.resolve(cwd, path));
|
|
136
|
+
if (input.tool === 'Bash') {
|
|
137
|
+
for (const invocation of judgedInvocations(input.invocations)) {
|
|
138
|
+
const names = resolved(invocation.cwd);
|
|
139
|
+
// `--out=cockpit.toml` names the file as surely as a bare operand does.
|
|
140
|
+
const naming = invocation.argv.flatMap((word, index) => names(word) || word.includes('=') && names(word.slice(word.indexOf('=') + 1)) ? [index] : []);
|
|
141
|
+
const writes = invocation.redirects.some(redirect => redirect.write);
|
|
142
|
+
if (!naming.length && !invocation.redirects.some(redirect => redirect.write && names(redirect.target)))
|
|
143
|
+
continue;
|
|
144
|
+
// A proven read leaves the file as it is, and staging or committing it records the text the
|
|
145
|
+
// guard already checked.
|
|
146
|
+
if (!writes && !invocation.contained && (classifyReadOnlyInvocation(invocation) || recordsWithoutWriting(invocation)))
|
|
147
|
+
continue;
|
|
148
|
+
// Installed cockpit reads the file it is given with --config, but a path or `node <script>`
|
|
149
|
+
// spells any program as well as cockpit. Whether it starts cockpit is the evaluator's
|
|
150
|
+
// launcher proof, so only the --config operand is left to it; naming the file anywhere
|
|
151
|
+
// else is denied whatever the spelling starts.
|
|
152
|
+
const prefix = invocation.executable === null || invocation.contained ? undefined : cockpitLauncherPrefix(invocation.executable, invocation.argv);
|
|
153
|
+
const configOperand = (index) => prefix !== undefined && index >= prefix
|
|
154
|
+
&& (invocation.argv[index].startsWith('--config=') || index > prefix && invocation.argv[index - 1] === '--config');
|
|
155
|
+
if (!writes && naming.every(configOperand))
|
|
156
|
+
continue;
|
|
157
|
+
return denial(`Refusing a shell command that names the cockpit configuration: the guard cannot tell whether it changes ${PINNED}, which only a person changes. Edit other tables with your editing tool, which the guard can check.`);
|
|
158
|
+
}
|
|
159
|
+
return undefined;
|
|
160
|
+
}
|
|
161
|
+
if (!input.paths.some(resolved(input.cwd)))
|
|
162
|
+
return undefined;
|
|
163
|
+
if (!input.content)
|
|
164
|
+
return denial(`Refusing an edit of the cockpit configuration whose new text the guard was not given, so it cannot show ${PINNED} unchanged.`);
|
|
165
|
+
const proposed = proposedText(input.content, resolved(input.cwd), current);
|
|
166
|
+
if (proposed === 'untouched')
|
|
167
|
+
return undefined;
|
|
168
|
+
if (proposed === undefined)
|
|
169
|
+
return denial(`Refusing an edit of the cockpit configuration that does not apply exactly once to its current text, so the guard cannot show ${PINNED} unchanged.`);
|
|
170
|
+
const before = pinnedPolicyTables(current);
|
|
171
|
+
const after = pinnedPolicyTables(proposed);
|
|
172
|
+
if (!before || !after)
|
|
173
|
+
return denial(`Refusing an edit that leaves the cockpit configuration unparseable, so the guard cannot show ${PINNED} unchanged.`);
|
|
174
|
+
if (!isDeepStrictEqual(before, after))
|
|
175
|
+
return denial(`Refusing to change ${PINNED} in the cockpit configuration: who may pass a gate is a person's decision, made outside the agent. Ask a person to edit it and run sync.`);
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { type ExecutionStep } from '../environment/contract.js';
|
|
2
|
+
import type { ResolvedExecutable } from '../environment/executable.js';
|
|
3
|
+
import { type ExecuteProbeOutcome } from '../environment-exec/execute.js';
|
|
4
|
+
/**
|
|
5
|
+
* The running cockpit package and every package it loads, each under the spelling it was found by
|
|
6
|
+
* and its real path, plus the consumer's `node_modules` spelling of the package when that is a link
|
|
7
|
+
* to it. The guard authenticates only its entry point, so all of these are controls.
|
|
8
|
+
*/
|
|
9
|
+
export declare function guardInstalledDirectories(packageRoot: string, consumerRoot: string): readonly string[];
|
|
10
|
+
/**
|
|
11
|
+
* The file whose identity proves that `executable` with the launcher `words` after it, run in `cwd`,
|
|
12
|
+
* starts `launcher`, the installed cockpit the guard runs from, or undefined when it may start
|
|
13
|
+
* something else. A path, or the script `node` is given, qualifies when it resolves to the launcher;
|
|
14
|
+
* the bare name when the first `cockpit` on the command search path does. `npx` qualifies only in
|
|
15
|
+
* `root`, where npm runs a `bin` the root package.json declares before any installed one and applies
|
|
16
|
+
* the root `.npmrc`, whose `script-shell` and `node-options` run other code first; with neither
|
|
17
|
+
* present it runs the root's `node_modules/.bin/cockpit`, which must resolve to the launcher.
|
|
18
|
+
*/
|
|
19
|
+
export declare function guardLauncherEvidence(executable: string, words: readonly string[], cwd: string, root: string, launcher: string, searchPath: string): string | undefined;
|
|
20
|
+
/**
|
|
21
|
+
* Absolute search path entries, and their real paths, that lie inside an allowed root. A relative
|
|
22
|
+
* entry resolves against whatever directory a command runs in, so it names no fixed location.
|
|
23
|
+
*/
|
|
24
|
+
export declare function guardSearchDirectories(searchPath: string, allowedRoots: readonly string[]): readonly string[];
|
|
25
|
+
/** The first entry of `searchPath` that is not absolute, an empty one included, or null when there is none. */
|
|
26
|
+
export declare function guardSearchPathRelativeEntry(searchPath: string): string | null;
|
|
27
|
+
/**
|
|
28
|
+
* Every path at or below `path` whose case-folded name starts with `.env` or is `.secrets`, of any
|
|
29
|
+
* type, without following links, as a recursive search would reach it; null once the walk passes
|
|
30
|
+
* `entryLimit` entries or finds more than the sensitive path bound. A missing path has none.
|
|
31
|
+
*/
|
|
32
|
+
export declare function guardSensitivePathsBelow(path: string, entryLimit?: number): readonly string[] | null;
|
|
33
|
+
export type GuardExecute = (step: ExecutionStep) => Promise<ExecuteProbeOutcome>;
|
|
34
|
+
/**
|
|
35
|
+
* The paths a default ripgrep search of `path` reads that are named `.env*`, lie below a `.secrets`
|
|
36
|
+
* directory or are one of `watched`, each reported at its first such name, as Git lists what the
|
|
37
|
+
* enclosing repository does not ignore; null past the sensitive path bound. Undefined when Git
|
|
38
|
+
* did not answer or its list cannot stand for ripgrep's: outside a repository, through a
|
|
39
|
+
* nested repository or submodule, below an `.ignore` or `.rgignore` file, which ripgrep reads and Git
|
|
40
|
+
* does not, or when the search names an ignored directory, whose children ripgrep may still read.
|
|
41
|
+
*
|
|
42
|
+
* Only `.gitignore` files are applied: ripgrep reads those, the repository's exclude file and the
|
|
43
|
+
* global excludes file, but finds the global one by other rules than Git, so Git applying fewer
|
|
44
|
+
* lists more. Hidden paths are kept: a type such as `rg -t sh`, which matches `.env`, reads them.
|
|
45
|
+
* Undefined as well when a `.gitignore` Git reads holds a line the two may match differently.
|
|
46
|
+
*/
|
|
47
|
+
export declare function guardUnignoredSensitivePaths(path: string, watched: readonly string[], git: ResolvedExecutable, execute: GuardExecute): Promise<readonly string[] | null | undefined>;
|
|
48
|
+
/**
|
|
49
|
+
* Every path `repository`'s index holds that is named `.env*`, lies below a `.secrets` or `.env*`
|
|
50
|
+
* directory or is one of `watched`, or null past the sensitive path bound or when Git did not
|
|
51
|
+
* answer. Only the index is asked: a file removed from it stays in history, which this does not walk.
|
|
52
|
+
*/
|
|
53
|
+
export declare function guardTrackedSensitivePaths(repository: string, watched: readonly string[], git: ResolvedExecutable, execute: GuardExecute): Promise<readonly string[] | null>;
|
|
54
|
+
/**
|
|
55
|
+
* Identities of every file in each case-folded spelling of `env/.secrets` below `root`, and whether
|
|
56
|
+
* any has a second hard link; `identities` is null once more than the bound of entries was seen,
|
|
57
|
+
* which leaves every read unproven rather than silently uncovered.
|
|
58
|
+
*/
|
|
59
|
+
export declare function guardSecretFiles(root: string): {
|
|
60
|
+
readonly identities: readonly string[] | null;
|
|
61
|
+
readonly linked: boolean;
|
|
62
|
+
};
|