diff --git a/.well-known/agents-shipgate.json b/.well-known/agents-shipgate.json index 91bfc257..e7d4713e 100644 --- a/.well-known/agents-shipgate.json +++ b/.well-known/agents-shipgate.json @@ -309,9 +309,9 @@ "attestation_schema_version": "0.5", "registry_schema_version": "0.4", "org_evidence_bundle_schema_version": "shipgate.org_evidence_bundle/v2", - "host_grants_inventory_schema_version": "0.7", - "host_grants_baseline_schema_version": "0.7", - "host_grants_drift_schema_version": "0.7", + "host_grants_inventory_schema_version": "0.8", + "host_grants_baseline_schema_version": "0.8", + "host_grants_drift_schema_version": "0.8", "trigger_catalog_schema_version": "0.4", "capability_standard_version": "0.5", "governance_benchmark_catalog_schema_version": "0.2", @@ -525,9 +525,9 @@ "org_governance": "https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/org-governance-schema.v0.1.json", "org_evidence_bundle": "https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/org-evidence-bundle-schema.v2.json", "registry": "https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/registry-schema.v0.4.json", - "host_grants_inventory": "https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/host-grants-inventory-schema.v0.7.json", - "host_grants_baseline": "https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/host-grants-baseline-schema.v0.7.json", - "host_grants_drift": "https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/host-grants-drift-schema.v0.7.json", + "host_grants_inventory": "https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/host-grants-inventory-schema.v0.8.json", + "host_grants_baseline": "https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/host-grants-baseline-schema.v0.8.json", + "host_grants_drift": "https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/host-grants-drift-schema.v0.8.json", "scenario": "https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/scenario-schema.v0.1.json", "checks_catalog": "https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/checks.json", "determinism_boundary": "https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/determinism-boundary.json", diff --git a/benchmark/cold-start/census.py b/benchmark/cold-start/census.py index dc03f5e6..ca75503f 100644 --- a/benchmark/cold-start/census.py +++ b/benchmark/cold-start/census.py @@ -67,6 +67,7 @@ # review is as much part of the boundary as the host files it reads. "shipgate.yaml", ".agents-shipgate/*", "policies/*.shipgate.yaml", ".shipgate/agent-contract.json", + ".shipgate/openshell.json", "**/.shipgate/openshell.json", ) #: Patterns whose files are host surface only when they carry particular diff --git a/docs/agent-contract-current.md b/docs/agent-contract-current.md index 1e5e5e6b..37a9b36d 100644 --- a/docs/agent-contract-current.md +++ b/docs/agent-contract-current.md @@ -820,7 +820,7 @@ Downstream repos generated with - Current attestation schema: `0.5` — [`docs/attestation-schema.v0.5.json`](attestation-schema.v0.5.json) - Current registry schema: `0.4` — [`docs/registry-schema.v0.4.json`](registry-schema.v0.4.json) - Current org evidence bundle schema: `shipgate.org_evidence_bundle/v2` — [`docs/org-evidence-bundle-schema.v2.json`](org-evidence-bundle-schema.v2.json) -- Current host-grants inventory, baseline, and drift schemas: `0.7` — [`inventory`](host-grants-inventory-schema.v0.7.json), [`baseline`](host-grants-baseline-schema.v0.7.json), [`drift`](host-grants-drift-schema.v0.7.json) +- Current host-grants inventory, baseline, and drift schemas: `0.8` — [`inventory`](host-grants-inventory-schema.v0.8.json), [`baseline`](host-grants-baseline-schema.v0.8.json), [`drift`](host-grants-drift-schema.v0.8.json). Version 0.8 adds selected OpenShell document facts; historical host schemas remain frozen. See [OpenShell support](openshell-support.md). - Current trigger catalog schema: `0.4` — [`docs/triggers.json`](triggers.json) - Current governance benchmark catalog schema: `0.2` — [`docs/governance-benchmark-catalog-schema.v0.2.json`](governance-benchmark-catalog-schema.v0.2.json) - Current governance benchmark result schema: `0.2` — [`docs/governance-benchmark-result-schema.v0.2.json`](governance-benchmark-result-schema.v0.2.json) diff --git a/docs/host-boundary-support.md b/docs/host-boundary-support.md index 785df461..fde2adf2 100644 --- a/docs/host-boundary-support.md +++ b/docs/host-boundary-support.md @@ -15,6 +15,7 @@ and `audit --host`. | Adapter | Status | Repository surfaces | Static semantics | |---|---|---|---| +| OpenShell | static inventory | root/nested `.shipgate/openshell.json` selecting arbitrary repository policy files | [Pinned policy schema 1 inventory](openshell-support.md), authored/effective-snapshot roles, explicit/defaulted fields and named read limits; runtime enforcement and freshness remain unverified | | Codex | first-class | `.codex/config.toml`, `.codex/hooks.json` | sandbox, approvals, network, MCP/app approvals, hooks | | Claude Code | first-class | `.claude/settings.json`, `.claude/settings.local.json`, `.mcp.json`, `CLAUDE.md`, Claude skills | permission modes/rules, sandbox/network, additional paths, MCP restrictions, plugins and their marketplaces (`extraKnownMarketplaces`), hooks | | Cursor | first-class | `.cursor/cli.json`, `.cursor/mcp.json`, `.cursor/rules/**` | Shell/Read/Write rules, MCP declarations, instruction trust roots | diff --git a/docs/host-grants-baseline-schema.v0.8.json b/docs/host-grants-baseline-schema.v0.8.json new file mode 100644 index 00000000..2b8e7029 --- /dev/null +++ b/docs/host-grants-baseline-schema.v0.8.json @@ -0,0 +1,2677 @@ +{ + "$defs": { + "HostAdditionalPathGrantV2": { + "additionalProperties": false, + "properties": { + "access": { + "enum": [ + "none", + "read", + "write", + "execute", + "external", + "admin", + "unknown" + ], + "title": "Access", + "type": "string" + }, + "config_sha256": { + "title": "Config Sha256", + "type": "string" + }, + "grant_id": { + "title": "Grant Id", + "type": "string" + }, + "host": { + "enum": [ + "codex", + "claude-code", + "cursor", + "vscode", + "github" + ], + "title": "Host", + "type": "string" + }, + "kind": { + "const": "additional_path", + "default": "additional_path", + "title": "Kind", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "risk": { + "enum": [ + "none", + "low", + "medium", + "high", + "critical", + "unknown" + ], + "title": "Risk", + "type": "string" + }, + "scope": { + "enum": [ + "repository", + "local_static" + ], + "title": "Scope", + "type": "string" + }, + "source": { + "title": "Source", + "type": "string" + } + }, + "required": [ + "grant_id", + "host", + "scope", + "source", + "config_sha256", + "access", + "risk", + "path" + ], + "title": "HostAdditionalPathGrantV2", + "type": "object" + }, + "HostArtifactV8": { + "additionalProperties": false, + "properties": { + "artifact_id": { + "title": "Artifact Id", + "type": "string" + }, + "host": { + "enum": [ + "codex", + "claude-code", + "cursor", + "vscode", + "github", + "openshell" + ], + "title": "Host", + "type": "string" + }, + "instruction_structure": { + "anyOf": [ + { + "$ref": "#/$defs/InstructionStructureEvidence" + }, + { + "type": "null" + } + ], + "default": null + }, + "kind": { + "enum": [ + "config", + "mcp", + "hooks", + "workflow", + "instructions", + "requirements", + "hook_script", + "openshell_selection", + "openshell_policy" + ], + "title": "Kind", + "type": "string" + }, + "parse_status": { + "enum": [ + "parsed", + "failed", + "unsupported" + ], + "title": "Parse Status", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "redacted_sha256": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Redacted Sha256" + }, + "resolved_through": { + "items": { + "type": "string" + }, + "title": "Resolved Through", + "type": "array" + }, + "scope": { + "enum": [ + "repository", + "local_static" + ], + "title": "Scope", + "type": "string" + } + }, + "required": [ + "artifact_id", + "host", + "scope", + "path", + "kind", + "parse_status" + ], + "title": "HostArtifactV8", + "type": "object" + }, + "HostCoverageV8": { + "additionalProperties": false, + "properties": { + "host": { + "enum": [ + "codex", + "claude-code", + "cursor", + "vscode", + "github", + "openshell" + ], + "title": "Host", + "type": "string" + }, + "issue_ids": { + "items": { + "type": "string" + }, + "title": "Issue Ids", + "type": "array" + }, + "scope": { + "enum": [ + "repository", + "local_static" + ], + "title": "Scope", + "type": "string" + }, + "sources_expected": { + "items": { + "type": "string" + }, + "title": "Sources Expected", + "type": "array" + }, + "sources_observed": { + "items": { + "type": "string" + }, + "title": "Sources Observed", + "type": "array" + }, + "status": { + "enum": [ + "complete", + "partial", + "experimental" + ], + "title": "Status", + "type": "string" + } + }, + "required": [ + "host", + "scope", + "status" + ], + "title": "HostCoverageV8", + "type": "object" + }, + "HostGrantsBaselineV8": { + "additionalProperties": false, + "properties": { + "host_grants_schema_version": { + "const": "0.8", + "default": "0.8", + "title": "Host Grants Schema Version", + "type": "string" + }, + "inventory": { + "$ref": "#/$defs/HostGrantsNormalizedSnapshotV8" + }, + "inventory_sha256": { + "title": "Inventory Sha256", + "type": "string" + }, + "scope": { + "enum": [ + "repository", + "local_static" + ], + "title": "Scope", + "type": "string" + } + }, + "required": [ + "scope", + "inventory_sha256", + "inventory" + ], + "title": "HostGrantsBaselineV8", + "type": "object" + }, + "HostGrantsNormalizedSnapshotV8": { + "additionalProperties": false, + "properties": { + "artifacts": { + "items": { + "$ref": "#/$defs/HostArtifactV8" + }, + "title": "Artifacts", + "type": "array" + }, + "grants": { + "items": { + "discriminator": { + "mapping": { + "additional_path": "#/$defs/HostAdditionalPathGrantV2", + "hook": "#/$defs/HostHookComparisonV7", + "instruction_trust_root": "#/$defs/HostInstructionGrantV2", + "mcp_server": "#/$defs/HostMcpServerGrantV2", + "openshell_policy": "#/$defs/HostOpenShellPolicyGrantV8", + "permission_mode": "#/$defs/HostPermissionModeGrantV2", + "permission_rule": "#/$defs/HostPermissionRuleGrantV2", + "plugin_or_app": "#/$defs/HostPluginGrantV2", + "profile": "#/$defs/HostProfileGrantV2", + "requirement": "#/$defs/HostRequirementGrantV2", + "sandbox": "#/$defs/HostSandboxGrantV2", + "workflow": "#/$defs/HostWorkflowGrantV7" + }, + "propertyName": "kind" + }, + "oneOf": [ + { + "$ref": "#/$defs/HostMcpServerGrantV2" + }, + { + "$ref": "#/$defs/HostPermissionRuleGrantV2" + }, + { + "$ref": "#/$defs/HostPermissionModeGrantV2" + }, + { + "$ref": "#/$defs/HostHookComparisonV7" + }, + { + "$ref": "#/$defs/HostSandboxGrantV2" + }, + { + "$ref": "#/$defs/HostAdditionalPathGrantV2" + }, + { + "$ref": "#/$defs/HostPluginGrantV2" + }, + { + "$ref": "#/$defs/HostProfileGrantV2" + }, + { + "$ref": "#/$defs/HostRequirementGrantV2" + }, + { + "$ref": "#/$defs/HostWorkflowGrantV7" + }, + { + "$ref": "#/$defs/HostInstructionGrantV2" + }, + { + "$ref": "#/$defs/HostOpenShellPolicyGrantV8" + } + ] + }, + "title": "Grants", + "type": "array" + }, + "host_coverage": { + "items": { + "$ref": "#/$defs/HostCoverageV8" + }, + "title": "Host Coverage", + "type": "array" + }, + "scope": { + "enum": [ + "repository", + "local_static" + ], + "title": "Scope", + "type": "string" + } + }, + "required": [ + "scope" + ], + "title": "HostGrantsNormalizedSnapshotV8", + "type": "object" + }, + "HostHookComparisonV7": { + "additionalProperties": false, + "properties": { + "access": { + "enum": [ + "none", + "read", + "write", + "execute", + "external", + "admin", + "unknown" + ], + "title": "Access", + "type": "string" + }, + "config_sha256": { + "title": "Config Sha256", + "type": "string" + }, + "event": { + "title": "Event", + "type": "string" + }, + "grant_id": { + "title": "Grant Id", + "type": "string" + }, + "host": { + "enum": [ + "codex", + "claude-code", + "cursor", + "vscode", + "github" + ], + "title": "Host", + "type": "string" + }, + "kind": { + "const": "hook", + "default": "hook", + "title": "Kind", + "type": "string" + }, + "risk": { + "enum": [ + "none", + "low", + "medium", + "high", + "critical", + "unknown" + ], + "title": "Risk", + "type": "string" + }, + "scope": { + "enum": [ + "repository", + "local_static" + ], + "title": "Scope", + "type": "string" + }, + "script_inputs": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/HostHookScriptInputV7" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Script Inputs" + }, + "source": { + "title": "Source", + "type": "string" + } + }, + "required": [ + "grant_id", + "host", + "scope", + "source", + "config_sha256", + "access", + "risk", + "event" + ], + "title": "HostHookComparisonV7", + "type": "object" + }, + "HostHookScriptInputV7": { + "additionalProperties": false, + "description": "A direct executable reference and its byte reading, never script semantics.", + "properties": { + "basis": { + "anyOf": [ + { + "enum": [ + "project_root_placeholder", + "plugin_root_placeholder", + "absolute_workspace_path" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Basis" + }, + "handler": { + "minimum": 0, + "title": "Handler", + "type": "integer" + }, + "limit": { + "anyOf": [ + { + "enum": [ + "unsupported_host", + "not_command_handler", + "unsupported_command_shape", + "platform_command_override", + "unsupported_shell", + "unsupported_exec_form", + "unsupported_shell_command", + "dynamic_command_argument", + "unexpanded_path_placeholder", + "unsupported_path_placeholder", + "plugin_root_not_established", + "unsupported_or_escaping_path", + "dynamic_or_conditional_path", + "working_directory_not_established", + "interpreter_wrapper", + "path_lookup", + "external_executable", + "unsupported_hook_shape", + "handler_bound_exceeded", + "hook_selection_not_established", + "redacted_dependency_path", + "escaping_path", + "missing_input", + "symlink_input", + "non_regular_input", + "oversized_input", + "unsafe_or_unreadable_input", + "unreadable_input" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Limit" + }, + "path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Path" + }, + "sha256": { + "anyOf": [ + { + "pattern": "^[0-9a-f]{64}$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Sha256" + }, + "size_bytes": { + "anyOf": [ + { + "minimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Size Bytes" + } + }, + "required": [ + "handler" + ], + "title": "HostHookScriptInputV7", + "type": "object" + }, + "HostInstructionGrantV2": { + "additionalProperties": false, + "properties": { + "access": { + "enum": [ + "none", + "read", + "write", + "execute", + "external", + "admin", + "unknown" + ], + "title": "Access", + "type": "string" + }, + "config_sha256": { + "title": "Config Sha256", + "type": "string" + }, + "grant_id": { + "title": "Grant Id", + "type": "string" + }, + "host": { + "enum": [ + "codex", + "claude-code", + "cursor", + "vscode", + "github" + ], + "title": "Host", + "type": "string" + }, + "kind": { + "const": "instruction_trust_root", + "default": "instruction_trust_root", + "title": "Kind", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "risk": { + "enum": [ + "none", + "low", + "medium", + "high", + "critical", + "unknown" + ], + "title": "Risk", + "type": "string" + }, + "scope": { + "enum": [ + "repository", + "local_static" + ], + "title": "Scope", + "type": "string" + }, + "source": { + "title": "Source", + "type": "string" + } + }, + "required": [ + "grant_id", + "host", + "scope", + "source", + "config_sha256", + "access", + "risk", + "path" + ], + "title": "HostInstructionGrantV2", + "type": "object" + }, + "HostMcpServerGrantV2": { + "additionalProperties": false, + "properties": { + "access": { + "enum": [ + "none", + "read", + "write", + "execute", + "external", + "admin", + "unknown" + ], + "title": "Access", + "type": "string" + }, + "config_sha256": { + "title": "Config Sha256", + "type": "string" + }, + "endpoint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Endpoint" + }, + "env_keys": { + "items": { + "type": "string" + }, + "title": "Env Keys", + "type": "array" + }, + "grant_id": { + "title": "Grant Id", + "type": "string" + }, + "header_keys": { + "items": { + "type": "string" + }, + "title": "Header Keys", + "type": "array" + }, + "host": { + "enum": [ + "codex", + "claude-code", + "cursor", + "vscode", + "github" + ], + "title": "Host", + "type": "string" + }, + "kind": { + "const": "mcp_server", + "default": "mcp_server", + "title": "Kind", + "type": "string" + }, + "risk": { + "enum": [ + "none", + "low", + "medium", + "high", + "critical", + "unknown" + ], + "title": "Risk", + "type": "string" + }, + "scope": { + "enum": [ + "repository", + "local_static" + ], + "title": "Scope", + "type": "string" + }, + "server": { + "title": "Server", + "type": "string" + }, + "source": { + "title": "Source", + "type": "string" + }, + "transport": { + "title": "Transport", + "type": "string" + } + }, + "required": [ + "grant_id", + "host", + "scope", + "source", + "config_sha256", + "access", + "risk", + "server", + "transport" + ], + "title": "HostMcpServerGrantV2", + "type": "object" + }, + "HostOpenShellPolicyGrantV8": { + "additionalProperties": false, + "properties": { + "access": { + "enum": [ + "none", + "read", + "write", + "execute", + "external", + "admin", + "unknown" + ], + "title": "Access", + "type": "string" + }, + "config_sha256": { + "title": "Config Sha256", + "type": "string" + }, + "facts": { + "$ref": "#/$defs/OpenShellPolicyFacts" + }, + "grant_id": { + "title": "Grant Id", + "type": "string" + }, + "host": { + "const": "openshell", + "default": "openshell", + "title": "Host", + "type": "string" + }, + "kind": { + "const": "openshell_policy", + "default": "openshell_policy", + "title": "Kind", + "type": "string" + }, + "risk": { + "enum": [ + "none", + "low", + "medium", + "high", + "critical", + "unknown" + ], + "title": "Risk", + "type": "string" + }, + "scope": { + "enum": [ + "repository", + "local_static" + ], + "title": "Scope", + "type": "string" + }, + "source": { + "title": "Source", + "type": "string" + } + }, + "required": [ + "grant_id", + "scope", + "source", + "config_sha256", + "access", + "risk", + "facts" + ], + "title": "HostOpenShellPolicyGrantV8", + "type": "object" + }, + "HostPermissionModeGrantV2": { + "additionalProperties": false, + "properties": { + "access": { + "enum": [ + "none", + "read", + "write", + "execute", + "external", + "admin", + "unknown" + ], + "title": "Access", + "type": "string" + }, + "config_sha256": { + "title": "Config Sha256", + "type": "string" + }, + "grant_id": { + "title": "Grant Id", + "type": "string" + }, + "host": { + "enum": [ + "codex", + "claude-code", + "cursor", + "vscode", + "github" + ], + "title": "Host", + "type": "string" + }, + "kind": { + "const": "permission_mode", + "default": "permission_mode", + "title": "Kind", + "type": "string" + }, + "risk": { + "enum": [ + "none", + "low", + "medium", + "high", + "critical", + "unknown" + ], + "title": "Risk", + "type": "string" + }, + "scope": { + "enum": [ + "repository", + "local_static" + ], + "title": "Scope", + "type": "string" + }, + "setting": { + "title": "Setting", + "type": "string" + }, + "source": { + "title": "Source", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "grant_id", + "host", + "scope", + "source", + "config_sha256", + "access", + "risk", + "setting", + "value" + ], + "title": "HostPermissionModeGrantV2", + "type": "object" + }, + "HostPermissionRuleGrantV2": { + "additionalProperties": false, + "properties": { + "access": { + "enum": [ + "none", + "read", + "write", + "execute", + "external", + "admin", + "unknown" + ], + "title": "Access", + "type": "string" + }, + "config_sha256": { + "title": "Config Sha256", + "type": "string" + }, + "disposition": { + "enum": [ + "allow", + "ask", + "deny" + ], + "title": "Disposition", + "type": "string" + }, + "grant_id": { + "title": "Grant Id", + "type": "string" + }, + "host": { + "enum": [ + "codex", + "claude-code", + "cursor", + "vscode", + "github" + ], + "title": "Host", + "type": "string" + }, + "kind": { + "const": "permission_rule", + "default": "permission_rule", + "title": "Kind", + "type": "string" + }, + "risk": { + "enum": [ + "none", + "low", + "medium", + "high", + "critical", + "unknown" + ], + "title": "Risk", + "type": "string" + }, + "rule": { + "title": "Rule", + "type": "string" + }, + "scope": { + "enum": [ + "repository", + "local_static" + ], + "title": "Scope", + "type": "string" + }, + "source": { + "title": "Source", + "type": "string" + }, + "wildcard": { + "default": false, + "title": "Wildcard", + "type": "boolean" + } + }, + "required": [ + "grant_id", + "host", + "scope", + "source", + "config_sha256", + "access", + "risk", + "disposition", + "rule" + ], + "title": "HostPermissionRuleGrantV2", + "type": "object" + }, + "HostPluginGrantV2": { + "additionalProperties": false, + "properties": { + "access": { + "enum": [ + "none", + "read", + "write", + "execute", + "external", + "admin", + "unknown" + ], + "title": "Access", + "type": "string" + }, + "config_sha256": { + "title": "Config Sha256", + "type": "string" + }, + "enabled": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Enabled" + }, + "grant_id": { + "title": "Grant Id", + "type": "string" + }, + "host": { + "enum": [ + "codex", + "claude-code", + "cursor", + "vscode", + "github" + ], + "title": "Host", + "type": "string" + }, + "kind": { + "const": "plugin_or_app", + "default": "plugin_or_app", + "title": "Kind", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "risk": { + "enum": [ + "none", + "low", + "medium", + "high", + "critical", + "unknown" + ], + "title": "Risk", + "type": "string" + }, + "scope": { + "enum": [ + "repository", + "local_static" + ], + "title": "Scope", + "type": "string" + }, + "source": { + "title": "Source", + "type": "string" + } + }, + "required": [ + "grant_id", + "host", + "scope", + "source", + "config_sha256", + "access", + "risk", + "name" + ], + "title": "HostPluginGrantV2", + "type": "object" + }, + "HostProfileGrantV2": { + "additionalProperties": false, + "properties": { + "access": { + "enum": [ + "none", + "read", + "write", + "execute", + "external", + "admin", + "unknown" + ], + "title": "Access", + "type": "string" + }, + "config_sha256": { + "title": "Config Sha256", + "type": "string" + }, + "grant_id": { + "title": "Grant Id", + "type": "string" + }, + "host": { + "enum": [ + "codex", + "claude-code", + "cursor", + "vscode", + "github" + ], + "title": "Host", + "type": "string" + }, + "kind": { + "const": "profile", + "default": "profile", + "title": "Kind", + "type": "string" + }, + "profile": { + "title": "Profile", + "type": "string" + }, + "resolved": { + "title": "Resolved", + "type": "boolean" + }, + "risk": { + "enum": [ + "none", + "low", + "medium", + "high", + "critical", + "unknown" + ], + "title": "Risk", + "type": "string" + }, + "scope": { + "enum": [ + "repository", + "local_static" + ], + "title": "Scope", + "type": "string" + }, + "source": { + "title": "Source", + "type": "string" + } + }, + "required": [ + "grant_id", + "host", + "scope", + "source", + "config_sha256", + "access", + "risk", + "profile", + "resolved" + ], + "title": "HostProfileGrantV2", + "type": "object" + }, + "HostRequirementGrantV2": { + "additionalProperties": false, + "properties": { + "access": { + "enum": [ + "none", + "read", + "write", + "execute", + "external", + "admin", + "unknown" + ], + "title": "Access", + "type": "string" + }, + "config_sha256": { + "title": "Config Sha256", + "type": "string" + }, + "grant_id": { + "title": "Grant Id", + "type": "string" + }, + "host": { + "enum": [ + "codex", + "claude-code", + "cursor", + "vscode", + "github" + ], + "title": "Host", + "type": "string" + }, + "kind": { + "const": "requirement", + "default": "requirement", + "title": "Kind", + "type": "string" + }, + "requirement": { + "title": "Requirement", + "type": "string" + }, + "risk": { + "enum": [ + "none", + "low", + "medium", + "high", + "critical", + "unknown" + ], + "title": "Risk", + "type": "string" + }, + "scope": { + "enum": [ + "repository", + "local_static" + ], + "title": "Scope", + "type": "string" + }, + "source": { + "title": "Source", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "grant_id", + "host", + "scope", + "source", + "config_sha256", + "access", + "risk", + "requirement", + "value" + ], + "title": "HostRequirementGrantV2", + "type": "object" + }, + "HostReusableWorkflowCallV6": { + "additionalProperties": false, + "properties": { + "job": { + "title": "Job", + "type": "string" + }, + "secret_mappings": { + "items": { + "$ref": "#/$defs/HostReusableWorkflowSecretV6" + }, + "title": "Secret Mappings", + "type": "array" + }, + "secrets_inherit": { + "title": "Secrets Inherit", + "type": "boolean" + }, + "uses": { + "title": "Uses", + "type": "string" + }, + "uses_redacted": { + "default": false, + "title": "Uses Redacted", + "type": "boolean" + } + }, + "required": [ + "job", + "uses", + "secrets_inherit" + ], + "title": "HostReusableWorkflowCallV6", + "type": "object" + }, + "HostReusableWorkflowSecretV6": { + "additionalProperties": false, + "description": "One named secret a job passes to the reusable workflow it calls (#693).\n\n``destination`` is the callee's secret input name as the caller writes it.\n``source`` is ``NAME`` from a whole-value ``${{ secrets.NAME }}``, and\n``form`` is then ``secret``. The name is a reference, never a value: it\ndoes not establish the secret's privilege, whether the caller has it, or\nwhat the called workflow does with it. Anything else is ``unresolved``,\nand none of its value is published or digested: a literal value, any\nother expression, a non-string, or a ``secrets`` that is neither\n``inherit`` nor a mapping (``destination`` is then ``null``). A name the\ncredential redactors rewrite is ``redacted`` and records a blocking\ncoverage issue, because two values that redact alike must never compare as\nunchanged. Every other unresolved mapping records a non-blocking one naming\nits ``job/destination``: only that value is uncompared, so the rest of the\nfile still compares.", + "properties": { + "destination": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Destination" + }, + "form": { + "enum": [ + "secret", + "unresolved" + ], + "title": "Form", + "type": "string" + }, + "source": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source" + }, + "unresolved_reason": { + "anyOf": [ + { + "enum": [ + "literal_value", + "expression", + "not_a_string", + "redacted", + "secrets_not_a_mapping" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Unresolved Reason" + } + }, + "required": [ + "destination", + "source", + "form" + ], + "title": "HostReusableWorkflowSecretV6", + "type": "object" + }, + "HostSandboxGrantV2": { + "additionalProperties": false, + "properties": { + "access": { + "enum": [ + "none", + "read", + "write", + "execute", + "external", + "admin", + "unknown" + ], + "title": "Access", + "type": "string" + }, + "config_sha256": { + "title": "Config Sha256", + "type": "string" + }, + "grant_id": { + "title": "Grant Id", + "type": "string" + }, + "host": { + "enum": [ + "codex", + "claude-code", + "cursor", + "vscode", + "github" + ], + "title": "Host", + "type": "string" + }, + "kind": { + "const": "sandbox", + "default": "sandbox", + "title": "Kind", + "type": "string" + }, + "risk": { + "enum": [ + "none", + "low", + "medium", + "high", + "critical", + "unknown" + ], + "title": "Risk", + "type": "string" + }, + "scope": { + "enum": [ + "repository", + "local_static" + ], + "title": "Scope", + "type": "string" + }, + "setting": { + "title": "Setting", + "type": "string" + }, + "source": { + "title": "Source", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "grant_id", + "host", + "scope", + "source", + "config_sha256", + "access", + "risk", + "setting", + "value" + ], + "title": "HostSandboxGrantV2", + "type": "object" + }, + "HostWorkflowAgentLaunchV7": { + "additionalProperties": false, + "description": "A step that launches a known coding agent, read as text and never run (#823).\n\n``agent`` is a documented action reference's ``owner/repo`` (the step's\n``uses:`` at any ref; the Claude base action also as the ``base-action``\ndirectory of ``anthropics/claude-code-action``), or a known agent CLI a\n``run:`` launches when the whole ``run:`` is one line of plain words\n(letters, digits and ``_ . / : = , % + -``, separated by spaces or tabs),\nrun by ``bash``, ``sh`` or the runner's default shell, whose program,\nafter any ``NAME=value`` assignments, has the file name ``claude`` and\npasses ``-p``/``--print``, or ``codex`` followed by ``exec`` (``e``).\n``form: read`` lists the documented permission inputs or flags the step\ndeclares in ``settings``, and the documented widening rules they meet in\n``widening_rules``, omitted when none. ``form: unresolved`` is an agent\naction whose ``with:`` is not a mapping (``inputs_not_a_mapping``), with\nno settings, and records a non-blocking coverage issue. Any other\n``run:`` that mentions an agent CLI is not a launch: it is listed in\n``unread_agent_runs``. ``job_secrets`` names the secrets the step's job\nreferences (``${{ secrets.NAME }}``) and the workflow-level ``env``\npasses: context for the row that names this step, never compared.\n``job`` and ``step`` are published labels (#802).", + "properties": { + "agent": { + "enum": [ + "anthropics/claude-code-action", + "anthropics/claude-code-base-action", + "anthropics/claude-code-action/base-action", + "openai/codex-action", + "claude", + "codex" + ], + "title": "Agent", + "type": "string" + }, + "form": { + "enum": [ + "read", + "unresolved" + ], + "title": "Form", + "type": "string" + }, + "job": { + "title": "Job", + "type": "string" + }, + "job_secrets": { + "items": { + "type": "string" + }, + "title": "Job Secrets", + "type": "array" + }, + "settings": { + "items": { + "$ref": "#/$defs/HostWorkflowAgentSettingV7" + }, + "title": "Settings", + "type": "array" + }, + "step": { + "title": "Step", + "type": "string" + }, + "unresolved_reason": { + "anyOf": [ + { + "const": "inputs_not_a_mapping", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Unresolved Reason" + }, + "widening_rules": { + "items": { + "$ref": "#/$defs/HostWorkflowAgentRuleV7" + }, + "title": "Widening Rules", + "type": "array" + } + }, + "required": [ + "job", + "step", + "agent", + "form" + ], + "title": "HostWorkflowAgentLaunchV7", + "type": "object" + }, + "HostWorkflowAgentRuleV7": { + "additionalProperties": false, + "description": "One documented widening rule an agent launch meets, and the setting it was read from (#823).\n\nDecided when the workflow is read, from the declared text, before any of\nit is withheld for publication, so redaction never hides a rule. Only\ntext this reader reads exactly meets one: ``claude_args`` or\n``codex-args`` only when it is a plain list of words (never when it holds\na ``${{ }}`` expression), the entries of a user gate that hold no\nexpression, and a mode or ``settings`` input that holds none. Claude Code\nsettings written as JSON in the ``settings`` input meet\n``bypass_permissions`` when their ``defaultMode`` is\n``bypassPermissions``, read as the settings reader reads it; a path to a\nsettings file is not read. ``setting`` is the input (``claude_args``,\n``allowed_bots``, ``sandbox``, ``permission-profile``, \u2026) or the CLI\nflag's primary spelling. One rule compares as one whatever setting meets\nit, except ``open_gate``, which is one rule per gate input.", + "properties": { + "rule": { + "enum": [ + "bypass_permissions", + "bypass_approvals_and_sandbox", + "danger_full_access", + "unsafe_safety_strategy", + "open_gate" + ], + "title": "Rule", + "type": "string" + }, + "setting": { + "title": "Setting", + "type": "string" + } + }, + "required": [ + "rule", + "setting" + ], + "title": "HostWorkflowAgentRuleV7", + "type": "object" + }, + "HostWorkflowAgentSettingV7": { + "additionalProperties": false, + "description": "One permission input or flag an agent launch declares, compared as text (#823).\n\n``name`` is the documented input (``claude_args``, ``sandbox``, \u2026) or the\nflag's primary spelling (``--allowedTools`` for ``--allowed-tools`` too).\n``value`` is the declared text, stripped, as it may be published; a flag\nthat takes no value has ``null``.\n\n``claude_args`` and ``codex-args`` are read only when they are a plain\nlist of words: letters, digits and ``_ . / : = , % + - ( )``, separated by\nblanks or newlines, with no ``--settings`` or ``--mcp-config`` flag. Every\nparser involved splits such text the same way, so it is published as\nthose words, one space apart. Any other value \u2014 holding a quote, a\n``${{ }}`` expression, ``$``, a backtick, a comment, a shell operator,\nJSON or another character \u2014 is ``unread_arguments``: ``value`` is\n````, a short digest, so an edit to it is still a change\nwhile none of its text is published; no documented widening rule is read\nfrom it; and it records a non-blocking coverage issue naming its\n``job/step`` (#823 review cycle 4). A codex ``--config`` override keeps\nits key; its value is ```` under ``env``, ``headers`` or a\nsecret-named key, as the host readers redact such values, published as\nwritten for ``sandbox_mode``, ``default_permissions``,\n``approval_policy`` and ``model``, and ```` otherwise.\n\nEvery other input is one value. A JSON object (a ``settings`` or\n``mcp_config`` value) publishes its shape and none of its free text: key\nnames, numbers, booleans and ``null``, with each string replaced by\n````, a short digest of what the host readers digest for it,\nso an edit to it is still a change. ``env`` and ``headers`` values,\n``apiKeyHelper`` and every secret-named value are ````, as the\nhost readers redact them. The strings a host reader publishes are kept:\na ``permissions.allow``/``ask``/``deny`` rule and a documented Claude\nCode setting's value such as ``defaultMode``, and an MCP server's command\nname and its URL's scheme and host, each followed by the digest when it\ndrops something the digest reads (a command's arguments, a URL's query).\nSo an MCP server's arguments and a hook's command publish nothing, as\n`.mcp.json` and `.claude/settings.json` do not (#823 review). A\n``settings`` or ``mcp_config`` value that neither starts like a JSON\nobject nor is a plain file path (path characters, and a ``${{ }}``\nexpression only as a plain context reference) is ````, a\ndigest and none of its text (#823 review cycle 5). A URL in\nother text publishes its scheme and host with ```` for its\npath and query (#723). Other text \u2014 a prompt, a flag's value \u2014 is\npublished through the workflow label redaction (#802). A value it\nrewrites is credential-shaped \u2014 a token, but also prose such as \"never\nprint bearer tokens\" \u2014 and is published redacted with\n``unresolved_reason: redacted``: it is compared as published, beside the\nrules read from its declared text, and records a non-blocking coverage\nissue naming its ``job/step``, because an edit inside what is redacted is\nnot reported. A value that is not a string (``not_a_string``), or one\nholding text that starts like JSON and does not parse (``unparsed_json``),\nis ``null`` and records a non-blocking coverage issue naming its\n``job/step``: it is neither published nor compared.\n\n``holds_expression`` is ``true`` when an input other than an argument\ninput holds a ``${{ }}`` expression, which GitHub substitutes before the\naction reads the input, and is omitted otherwise. A documented widening\nrule is then read only from the entries of a user gate that hold none,\nand from no mode or settings input, and a rule the launch gains in the\nsame job afterwards is not claimed, because the substituted text may\nalready have met it.", + "properties": { + "holds_expression": { + "default": false, + "title": "Holds Expression", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "unresolved_reason": { + "anyOf": [ + { + "enum": [ + "not_a_string", + "redacted", + "unparsed_json", + "unread_arguments" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Unresolved Reason" + }, + "value": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Value" + } + }, + "required": [ + "name", + "value" + ], + "title": "HostWorkflowAgentSettingV7", + "type": "object" + }, + "HostWorkflowCheckoutRefV7": { + "additionalProperties": false, + "description": "One ``actions/checkout`` step and the ``with.ref`` it declares, as text (#823).\n\n``ref`` is ``null`` when the step declares none, or an empty one: the\ncheckout's default for the triggering event. A ref the label redaction\nrewrites is published redacted with ``unresolved_reason: redacted`` and\nmakes the workflow a blocking limit, as a redacted step reference does\n(#767): a ref names the code the job runs, as a step reference does. A\nvalue that is not a string, or ``with:`` that is not a mapping,\nis ``null`` with ``unresolved_reason`` and records a non-blocking coverage\nissue. The ref is never resolved or fetched.", + "properties": { + "job": { + "title": "Job", + "type": "string" + }, + "ref": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Ref" + }, + "step": { + "title": "Step", + "type": "string" + }, + "unresolved_reason": { + "anyOf": [ + { + "enum": [ + "not_a_string", + "redacted", + "inputs_not_a_mapping" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Unresolved Reason" + } + }, + "required": [ + "job", + "step", + "ref" + ], + "title": "HostWorkflowCheckoutRefV7", + "type": "object" + }, + "HostWorkflowGrantV7": { + "additionalProperties": false, + "description": "A v0.6 workflow grant plus the agent launches, unread agent steps and checkout refs its steps declare.\n\nEach list is present only when a step declares one. In a v0.7 grant an\nabsent list means the steps were read and declare none; the schema\nversion, not the key, separates that from a legacy grant that never read\nthem. ``unread_agent_runs`` is a named limit and is never compared.\n``access`` and ``risk`` still describe the workflow's token and triggers\nalone.", + "properties": { + "access": { + "enum": [ + "none", + "read", + "write", + "execute", + "external", + "admin", + "unknown" + ], + "title": "Access", + "type": "string" + }, + "agent_launches": { + "items": { + "$ref": "#/$defs/HostWorkflowAgentLaunchV7" + }, + "title": "Agent Launches", + "type": "array" + }, + "checkout_refs": { + "items": { + "$ref": "#/$defs/HostWorkflowCheckoutRefV7" + }, + "title": "Checkout Refs", + "type": "array" + }, + "config_sha256": { + "title": "Config Sha256", + "type": "string" + }, + "effective_write_scopes": { + "items": { + "type": "string" + }, + "title": "Effective Write Scopes", + "type": "array" + }, + "grant_id": { + "title": "Grant Id", + "type": "string" + }, + "host": { + "enum": [ + "codex", + "claude-code", + "cursor", + "vscode", + "github" + ], + "title": "Host", + "type": "string" + }, + "kind": { + "const": "workflow", + "default": "workflow", + "title": "Kind", + "type": "string" + }, + "permission_contexts": { + "items": { + "$ref": "#/$defs/HostWorkflowPermissionsV4" + }, + "title": "Permission Contexts", + "type": "array" + }, + "pull_request_target": { + "default": false, + "title": "Pull Request Target", + "type": "boolean" + }, + "reusable_calls": { + "items": { + "$ref": "#/$defs/HostReusableWorkflowCallV6" + }, + "title": "Reusable Calls", + "type": "array" + }, + "risk": { + "enum": [ + "none", + "low", + "medium", + "high", + "critical", + "unknown" + ], + "title": "Risk", + "type": "string" + }, + "scope": { + "enum": [ + "repository", + "local_static" + ], + "title": "Scope", + "type": "string" + }, + "source": { + "title": "Source", + "type": "string" + }, + "step_actions": { + "items": { + "$ref": "#/$defs/HostWorkflowStepActionV6" + }, + "title": "Step Actions", + "type": "array" + }, + "triggers": { + "items": { + "type": "string" + }, + "title": "Triggers", + "type": "array" + }, + "unread_agent_runs": { + "items": { + "$ref": "#/$defs/HostWorkflowUnreadAgentRunV7" + }, + "title": "Unread Agent Runs", + "type": "array" + }, + "write_all": { + "default": false, + "title": "Write All", + "type": "boolean" + }, + "write_scopes": { + "items": { + "type": "string" + }, + "title": "Write Scopes", + "type": "array" + } + }, + "required": [ + "grant_id", + "host", + "scope", + "source", + "config_sha256", + "access", + "risk", + "permission_contexts", + "effective_write_scopes", + "reusable_calls" + ], + "title": "HostWorkflowGrantV7", + "type": "object" + }, + "HostWorkflowPermissionsV4": { + "additionalProperties": false, + "properties": { + "job": { + "title": "Job", + "type": "string" + }, + "permissions": { + "additionalProperties": { + "enum": [ + "read", + "write" + ], + "type": "string" + }, + "title": "Permissions", + "type": "object" + }, + "state": { + "enum": [ + "explicit", + "repository_default", + "unresolved" + ], + "title": "State", + "type": "string" + } + }, + "required": [ + "job", + "state", + "permissions" + ], + "title": "HostWorkflowPermissionsV4", + "type": "object" + }, + "HostWorkflowStepActionV6": { + "additionalProperties": false, + "description": "One step's declared action reference, read as text and never fetched.\n\n``form`` is ``remote`` for ``owner/repo[/path]@ref``, ``docker`` for\n``docker://\u2026``, and ``unresolved`` for a value Shipgate does not resolve\nto an action identity; ``unresolved_reason`` then says which. A job whose\n``steps`` is not a list, or a step that is not a mapping, is listed as\nunresolved too, with no ``uses``, so an absent list still means the steps\nwere read and declare nothing. A local\n``./\u2026`` reference is not listed: composite actions remain unread (#701).\n``step`` is the step's ``id``, else its ``name``, else ``steps[N]`` \u2014 the\nevidence a reviewer uses to find it, not part of the comparison. ``job``\nand ``step`` are published labels: credential-shaped text in either, and\nthe userinfo of any ``scheme://\u2026@`` inside it, is redacted (#802).", + "properties": { + "form": { + "enum": [ + "remote", + "docker", + "unresolved" + ], + "title": "Form", + "type": "string" + }, + "job": { + "title": "Job", + "type": "string" + }, + "step": { + "title": "Step", + "type": "string" + }, + "unresolved_reason": { + "anyOf": [ + { + "enum": [ + "expression", + "unsupported_reference", + "not_a_string", + "redacted", + "steps_not_a_list", + "step_not_a_mapping" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Unresolved Reason" + }, + "uses": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Uses" + } + }, + "required": [ + "job", + "step", + "uses", + "form" + ], + "title": "HostWorkflowStepActionV6", + "type": "object" + }, + "HostWorkflowUnreadAgentRunV7": { + "additionalProperties": false, + "description": "A ``run:`` step that mentions a known agent CLI and is not read as an agent launch (#823 review cycle 4).\n\nAny ``run:`` holding ``claude`` or ``codex`` as a word of its own that is\nnot an agent launch this reader reads \u2014 more than one line or command, a\nquote, an expansion, a redirection, a comment, a continuation, a\n``${{ }}`` expression, another program such as ``npx`` or ``timeout``, a\nsubcommand that is not a headless launch, or a declared ``shell:`` other\nthan ``bash`` or ``sh`` run on the script alone (so ``bash -c '\u2026' {0}``\ntoo) \u2014 once for each agent CLI it mentions. It is a\nnamed, non-blocking limit and nothing more: none of the step's text is\npublished, it is never compared, so adding, removing or editing it gives\nno row, and it never says that the step starts, or does not start, an\nagent. ``job`` and ``step`` are published labels (#802).", + "properties": { + "agent": { + "enum": [ + "claude", + "codex" + ], + "title": "Agent", + "type": "string" + }, + "job": { + "title": "Job", + "type": "string" + }, + "step": { + "title": "Step", + "type": "string" + } + }, + "required": [ + "job", + "step", + "agent" + ], + "title": "HostWorkflowUnreadAgentRunV7", + "type": "object" + }, + "InstructionStructureEvidence": { + "additionalProperties": false, + "properties": { + "profile": { + "title": "Profile", + "type": "string" + }, + "reason": { + "title": "Reason", + "type": "string" + }, + "sha256": { + "anyOf": [ + { + "pattern": "^sha256:[0-9a-f]{64}$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Sha256" + }, + "status": { + "enum": [ + "guidance", + "structured", + "unresolved" + ], + "title": "Status", + "type": "string" + } + }, + "required": [ + "profile", + "status", + "reason" + ], + "title": "InstructionStructureEvidence", + "type": "object" + }, + "OpenShellAllowRule": { + "additionalProperties": false, + "properties": { + "allow": { + "$ref": "#/$defs/OpenShellRequestMatcher" + } + }, + "required": [ + "allow" + ], + "title": "OpenShellAllowRule", + "type": "object" + }, + "OpenShellAnyMatcher": { + "additionalProperties": false, + "properties": { + "any": { + "items": { + "type": "string" + }, + "minItems": 1, + "title": "Any", + "type": "array" + } + }, + "required": [ + "any" + ], + "title": "OpenShellAnyMatcher", + "type": "object" + }, + "OpenShellBinary": { + "additionalProperties": false, + "properties": { + "path": { + "title": "Path", + "type": "string" + } + }, + "required": [ + "path" + ], + "title": "OpenShellBinary", + "type": "object" + }, + "OpenShellCredentialBinding": { + "additionalProperties": false, + "properties": { + "provider": { + "title": "Provider", + "type": "string" + } + }, + "required": [ + "provider" + ], + "title": "OpenShellCredentialBinding", + "type": "object" + }, + "OpenShellEndpoint": { + "additionalProperties": false, + "properties": { + "access": { + "default": "", + "enum": [ + "", + "read-only", + "read-write", + "full" + ], + "title": "Access", + "type": "string" + }, + "allow_encoded_slash": { + "default": false, + "title": "Allow Encoded Slash", + "type": "boolean" + }, + "allow_uninspected_credentials": { + "default": false, + "title": "Allow Uninspected Credentials", + "type": "boolean" + }, + "allowed_ips": { + "items": { + "type": "string" + }, + "title": "Allowed Ips", + "type": "array" + }, + "credential_binding": { + "anyOf": [ + { + "$ref": "#/$defs/OpenShellCredentialBinding" + }, + { + "type": "null" + } + ], + "default": null + }, + "credential_signing": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Credential Signing" + }, + "deny_rules": { + "items": { + "$ref": "#/$defs/OpenShellRequestMatcher" + }, + "title": "Deny Rules", + "type": "array" + }, + "enforcement": { + "default": "audit", + "enum": [ + "audit", + "enforce", + "" + ], + "title": "Enforcement", + "type": "string" + }, + "graphql_max_body_bytes": { + "default": 65536, + "maximum": 4294967295, + "minimum": 0, + "title": "Graphql Max Body Bytes", + "type": "integer" + }, + "graphql_persisted_queries": { + "additionalProperties": { + "$ref": "#/$defs/OpenShellGraphqlOperation" + }, + "title": "Graphql Persisted Queries", + "type": "object" + }, + "host": { + "default": "", + "title": "Host", + "type": "string" + }, + "json_rpc": { + "anyOf": [ + { + "$ref": "#/$defs/OpenShellJsonRpcOptions" + }, + { + "type": "null" + } + ], + "default": null + }, + "mcp": { + "anyOf": [ + { + "$ref": "#/$defs/OpenShellMcpOptions" + }, + { + "type": "null" + } + ], + "default": null + }, + "path": { + "default": "", + "title": "Path", + "type": "string" + }, + "persisted_queries": { + "default": "deny", + "enum": [ + "", + "deny", + "allow_registered" + ], + "title": "Persisted Queries", + "type": "string" + }, + "port": { + "default": 0, + "maximum": 65535, + "minimum": 0, + "title": "Port", + "type": "integer" + }, + "ports": { + "items": { + "type": "integer" + }, + "title": "Ports", + "type": "array" + }, + "protocol": { + "default": "", + "enum": [ + "", + "rest", + "websocket", + "graphql", + "mcp", + "json-rpc", + "tcp" + ], + "title": "Protocol", + "type": "string" + }, + "request_body_credential_rewrite": { + "default": false, + "title": "Request Body Credential Rewrite", + "type": "boolean" + }, + "rules": { + "items": { + "$ref": "#/$defs/OpenShellAllowRule" + }, + "title": "Rules", + "type": "array" + }, + "signing_region": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Signing Region" + }, + "signing_service": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Signing Service" + }, + "tls": { + "default": "", + "enum": [ + "", + "skip" + ], + "title": "Tls", + "type": "string" + }, + "websocket_credential_rewrite": { + "default": false, + "title": "Websocket Credential Rewrite", + "type": "boolean" + } + }, + "title": "OpenShellEndpoint", + "type": "object" + }, + "OpenShellFilesystem": { + "additionalProperties": false, + "properties": { + "include_workdir": { + "default": false, + "title": "Include Workdir", + "type": "boolean" + }, + "read_only": { + "items": { + "type": "string" + }, + "title": "Read Only", + "type": "array" + }, + "read_write": { + "items": { + "type": "string" + }, + "title": "Read Write", + "type": "array" + } + }, + "title": "OpenShellFilesystem", + "type": "object" + }, + "OpenShellGraphqlOperation": { + "additionalProperties": false, + "properties": { + "fields": { + "items": { + "type": "string" + }, + "title": "Fields", + "type": "array" + }, + "operation_name": { + "default": "", + "title": "Operation Name", + "type": "string" + }, + "operation_type": { + "default": "", + "title": "Operation Type", + "type": "string" + } + }, + "title": "OpenShellGraphqlOperation", + "type": "object" + }, + "OpenShellJsonRpcOptions": { + "additionalProperties": false, + "properties": { + "max_body_bytes": { + "default": 65536, + "maximum": 4294967295, + "minimum": 0, + "title": "Max Body Bytes", + "type": "integer" + } + }, + "title": "OpenShellJsonRpcOptions", + "type": "object" + }, + "OpenShellLandlock": { + "additionalProperties": false, + "properties": { + "compatibility": { + "default": "best_effort", + "enum": [ + "best_effort", + "hard_requirement" + ], + "title": "Compatibility", + "type": "string" + } + }, + "title": "OpenShellLandlock", + "type": "object" + }, + "OpenShellMcpOptions": { + "additionalProperties": false, + "properties": { + "allow_all_known_mcp_methods": { + "default": false, + "title": "Allow All Known Mcp Methods", + "type": "boolean" + }, + "max_body_bytes": { + "default": 65536, + "maximum": 4294967295, + "minimum": 0, + "title": "Max Body Bytes", + "type": "integer" + }, + "strict_tool_names": { + "default": true, + "title": "Strict Tool Names", + "type": "boolean" + }, + "versions": { + "items": { + "type": "string" + }, + "title": "Versions", + "type": "array" + } + }, + "title": "OpenShellMcpOptions", + "type": "object" + }, + "OpenShellNetworkRule": { + "additionalProperties": false, + "properties": { + "binaries": { + "items": { + "$ref": "#/$defs/OpenShellBinary" + }, + "title": "Binaries", + "type": "array" + }, + "endpoints": { + "items": { + "$ref": "#/$defs/OpenShellEndpoint" + }, + "title": "Endpoints", + "type": "array" + }, + "name": { + "default": "", + "title": "Name", + "type": "string" + } + }, + "title": "OpenShellNetworkRule", + "type": "object" + }, + "OpenShellPolicy": { + "additionalProperties": false, + "properties": { + "filesystem_policy": { + "anyOf": [ + { + "$ref": "#/$defs/OpenShellFilesystem" + }, + { + "type": "null" + } + ], + "default": null + }, + "landlock": { + "anyOf": [ + { + "$ref": "#/$defs/OpenShellLandlock" + }, + { + "type": "null" + } + ], + "default": null + }, + "network_middlewares": { + "additionalProperties": true, + "title": "Network Middlewares", + "type": "object" + }, + "network_policies": { + "additionalProperties": { + "$ref": "#/$defs/OpenShellNetworkRule" + }, + "title": "Network Policies", + "type": "object" + }, + "process": { + "anyOf": [ + { + "$ref": "#/$defs/OpenShellProcess" + }, + { + "type": "null" + } + ], + "default": null + }, + "version": { + "const": 1, + "title": "Version", + "type": "integer" + } + }, + "required": [ + "version" + ], + "title": "OpenShellPolicy", + "type": "object" + }, + "OpenShellPolicyFacts": { + "additionalProperties": false, + "properties": { + "defaulted_fields": { + "items": { + "type": "string" + }, + "title": "Defaulted Fields", + "type": "array" + }, + "field_paths": { + "items": { + "type": "string" + }, + "title": "Field Paths", + "type": "array" + }, + "filesystem_baseline": { + "const": "runtime_dependent_not_resolved", + "default": "runtime_dependent_not_resolved", + "title": "Filesystem Baseline", + "type": "string" + }, + "include_workdir_when_filesystem_omitted": { + "const": true, + "default": true, + "title": "Include Workdir When Filesystem Omitted", + "type": "boolean" + }, + "landlock_when_omitted": { + "const": "best_effort", + "default": "best_effort", + "title": "Landlock When Omitted", + "type": "string" + }, + "policy": { + "$ref": "#/$defs/OpenShellPolicy" + }, + "policy_schema_version": { + "const": 1, + "default": 1, + "title": "Policy Schema Version", + "type": "integer" + }, + "process_omission": { + "const": "driver_default", + "default": "driver_default", + "title": "Process Omission", + "type": "string" + }, + "registration": { + "title": "Registration", + "type": "string" + }, + "role": { + "enum": [ + "authored", + "effective_snapshot" + ], + "title": "Role", + "type": "string" + }, + "runtime_freshness_verified": { + "const": false, + "default": false, + "title": "Runtime Freshness Verified", + "type": "boolean" + }, + "runtime_version": { + "const": "0.1.2", + "title": "Runtime Version", + "type": "string" + } + }, + "required": [ + "registration", + "role", + "runtime_version", + "policy" + ], + "title": "OpenShellPolicyFacts", + "type": "object" + }, + "OpenShellProcess": { + "additionalProperties": false, + "properties": { + "run_as_group": { + "default": "", + "title": "Run As Group", + "type": "string" + }, + "run_as_user": { + "default": "", + "title": "Run As User", + "type": "string" + } + }, + "title": "OpenShellProcess", + "type": "object" + }, + "OpenShellRequestMatcher": { + "additionalProperties": false, + "properties": { + "command": { + "default": "", + "title": "Command", + "type": "string" + }, + "fields": { + "items": { + "type": "string" + }, + "title": "Fields", + "type": "array" + }, + "method": { + "default": "", + "title": "Method", + "type": "string" + }, + "operation_name": { + "default": "", + "title": "Operation Name", + "type": "string" + }, + "operation_type": { + "default": "", + "title": "Operation Type", + "type": "string" + }, + "params": { + "additionalProperties": { + "anyOf": [ + { + "type": "string" + }, + { + "$ref": "#/$defs/OpenShellAnyMatcher" + } + ] + }, + "title": "Params", + "type": "object" + }, + "path": { + "default": "", + "title": "Path", + "type": "string" + }, + "query": { + "additionalProperties": { + "anyOf": [ + { + "type": "string" + }, + { + "$ref": "#/$defs/OpenShellAnyMatcher" + } + ] + }, + "title": "Query", + "type": "object" + }, + "tool": { + "anyOf": [ + { + "type": "string" + }, + { + "$ref": "#/$defs/OpenShellAnyMatcher" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Tool" + } + }, + "title": "OpenShellRequestMatcher", + "type": "object" + } + }, + "$id": "https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/host-grants-baseline-schema.v0.8.json", + "$ref": "#/$defs/HostGrantsBaselineV8", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "JSON Schema for a human-acknowledged, scope-bound host-grants baseline.", + "title": "Agents Shipgate Host Grants Baseline v0.8" +} diff --git a/docs/host-grants-drift-schema.v0.8.json b/docs/host-grants-drift-schema.v0.8.json new file mode 100644 index 00000000..8a8639dc --- /dev/null +++ b/docs/host-grants-drift-schema.v0.8.json @@ -0,0 +1,455 @@ +{ + "$defs": { + "HostArtifactChangeV8": { + "additionalProperties": false, + "properties": { + "artifact_id": { + "title": "Artifact Id", + "type": "string" + }, + "baseline": { + "anyOf": [ + { + "$ref": "#/$defs/HostArtifactV8" + }, + { + "type": "null" + } + ], + "default": null + }, + "current": { + "anyOf": [ + { + "$ref": "#/$defs/HostArtifactV8" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "artifact_id" + ], + "title": "HostArtifactChangeV8", + "type": "object" + }, + "HostArtifactV8": { + "additionalProperties": false, + "properties": { + "artifact_id": { + "title": "Artifact Id", + "type": "string" + }, + "host": { + "enum": [ + "codex", + "claude-code", + "cursor", + "vscode", + "github", + "openshell" + ], + "title": "Host", + "type": "string" + }, + "instruction_structure": { + "anyOf": [ + { + "$ref": "#/$defs/InstructionStructureEvidence" + }, + { + "type": "null" + } + ], + "default": null + }, + "kind": { + "enum": [ + "config", + "mcp", + "hooks", + "workflow", + "instructions", + "requirements", + "hook_script", + "openshell_selection", + "openshell_policy" + ], + "title": "Kind", + "type": "string" + }, + "parse_status": { + "enum": [ + "parsed", + "failed", + "unsupported" + ], + "title": "Parse Status", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "redacted_sha256": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Redacted Sha256" + }, + "resolved_through": { + "items": { + "type": "string" + }, + "title": "Resolved Through", + "type": "array" + }, + "scope": { + "enum": [ + "repository", + "local_static" + ], + "title": "Scope", + "type": "string" + } + }, + "required": [ + "artifact_id", + "host", + "scope", + "path", + "kind", + "parse_status" + ], + "title": "HostArtifactV8", + "type": "object" + }, + "HostCoverageChangeV8": { + "additionalProperties": false, + "properties": { + "baseline": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Baseline" + }, + "current": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Current" + }, + "host": { + "enum": [ + "codex", + "claude-code", + "cursor", + "vscode", + "github", + "openshell" + ], + "title": "Host", + "type": "string" + } + }, + "required": [ + "host" + ], + "title": "HostCoverageChangeV8", + "type": "object" + }, + "HostGrantChangeV2": { + "additionalProperties": false, + "properties": { + "baseline": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Baseline" + }, + "current": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Current" + }, + "grant_id": { + "title": "Grant Id", + "type": "string" + } + }, + "required": [ + "grant_id" + ], + "title": "HostGrantChangeV2", + "type": "object" + }, + "HostGrantsDriftV8": { + "additionalProperties": false, + "properties": { + "artifact_changes": { + "items": { + "$ref": "#/$defs/HostArtifactChangeV8" + }, + "title": "Artifact Changes", + "type": "array" + }, + "baseline_file": { + "title": "Baseline File", + "type": "string" + }, + "baseline_sha256": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Baseline Sha256" + }, + "changes": { + "items": { + "$ref": "#/$defs/HostGrantChangeV2" + }, + "title": "Changes", + "type": "array" + }, + "comparison_status": { + "enum": [ + "comparable", + "incomparable" + ], + "title": "Comparison Status", + "type": "string" + }, + "coverage_changes": { + "items": { + "$ref": "#/$defs/HostCoverageChangeV8" + }, + "title": "Coverage Changes", + "type": "array" + }, + "current_sha256": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Current Sha256" + }, + "expansion_signals": { + "items": { + "type": "string" + }, + "title": "Expansion Signals", + "type": "array" + }, + "has_drift": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Has Drift" + }, + "host_grants_schema_version": { + "const": "0.8", + "default": "0.8", + "title": "Host Grants Schema Version", + "type": "string" + }, + "incomparable_reasons": { + "items": { + "type": "string" + }, + "title": "Incomparable Reasons", + "type": "array" + }, + "issues": { + "items": { + "$ref": "#/$defs/HostInventoryIssueV8" + }, + "title": "Issues", + "type": "array" + }, + "next_action": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Next Action" + }, + "scope": { + "enum": [ + "repository", + "local_static" + ], + "title": "Scope", + "type": "string" + } + }, + "required": [ + "baseline_file", + "scope", + "comparison_status", + "has_drift" + ], + "title": "HostGrantsDriftV8", + "type": "object" + }, + "HostInventoryIssueV8": { + "additionalProperties": false, + "properties": { + "blocking": { + "title": "Blocking", + "type": "boolean" + }, + "host": { + "enum": [ + "codex", + "claude-code", + "cursor", + "vscode", + "github", + "openshell" + ], + "title": "Host", + "type": "string" + }, + "issue_id": { + "title": "Issue Id", + "type": "string" + }, + "kind": { + "enum": [ + "parse_failed", + "unreadable", + "unsupported", + "unresolved_precedence", + "dynamic_source_excluded", + "remote_source_excluded" + ], + "title": "Kind", + "type": "string" + }, + "message": { + "title": "Message", + "type": "string" + }, + "source": { + "title": "Source", + "type": "string" + } + }, + "required": [ + "issue_id", + "kind", + "host", + "source", + "message", + "blocking" + ], + "title": "HostInventoryIssueV8", + "type": "object" + }, + "InstructionStructureEvidence": { + "additionalProperties": false, + "properties": { + "profile": { + "title": "Profile", + "type": "string" + }, + "reason": { + "title": "Reason", + "type": "string" + }, + "sha256": { + "anyOf": [ + { + "pattern": "^sha256:[0-9a-f]{64}$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Sha256" + }, + "status": { + "enum": [ + "guidance", + "structured", + "unresolved" + ], + "title": "Status", + "type": "string" + } + }, + "required": [ + "profile", + "status", + "reason" + ], + "title": "InstructionStructureEvidence", + "type": "object" + } + }, + "$id": "https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/host-grants-drift-schema.v0.8.json", + "$ref": "#/$defs/HostGrantsDriftV8", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "JSON Schema for scope-aware host-grant drift and incomparability.", + "title": "Agents Shipgate Host Grants Drift v0.8" +} diff --git a/docs/host-grants-inventory-schema.v0.8.json b/docs/host-grants-inventory-schema.v0.8.json new file mode 100644 index 00000000..2248fefb --- /dev/null +++ b/docs/host-grants-inventory-schema.v0.8.json @@ -0,0 +1,2922 @@ +{ + "$defs": { + "HostAdditionalPathGrantV2": { + "additionalProperties": false, + "properties": { + "access": { + "enum": [ + "none", + "read", + "write", + "execute", + "external", + "admin", + "unknown" + ], + "title": "Access", + "type": "string" + }, + "config_sha256": { + "title": "Config Sha256", + "type": "string" + }, + "grant_id": { + "title": "Grant Id", + "type": "string" + }, + "host": { + "enum": [ + "codex", + "claude-code", + "cursor", + "vscode", + "github" + ], + "title": "Host", + "type": "string" + }, + "kind": { + "const": "additional_path", + "default": "additional_path", + "title": "Kind", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "risk": { + "enum": [ + "none", + "low", + "medium", + "high", + "critical", + "unknown" + ], + "title": "Risk", + "type": "string" + }, + "scope": { + "enum": [ + "repository", + "local_static" + ], + "title": "Scope", + "type": "string" + }, + "source": { + "title": "Source", + "type": "string" + } + }, + "required": [ + "grant_id", + "host", + "scope", + "source", + "config_sha256", + "access", + "risk", + "path" + ], + "title": "HostAdditionalPathGrantV2", + "type": "object" + }, + "HostArtifactV8": { + "additionalProperties": false, + "properties": { + "artifact_id": { + "title": "Artifact Id", + "type": "string" + }, + "host": { + "enum": [ + "codex", + "claude-code", + "cursor", + "vscode", + "github", + "openshell" + ], + "title": "Host", + "type": "string" + }, + "instruction_structure": { + "anyOf": [ + { + "$ref": "#/$defs/InstructionStructureEvidence" + }, + { + "type": "null" + } + ], + "default": null + }, + "kind": { + "enum": [ + "config", + "mcp", + "hooks", + "workflow", + "instructions", + "requirements", + "hook_script", + "openshell_selection", + "openshell_policy" + ], + "title": "Kind", + "type": "string" + }, + "parse_status": { + "enum": [ + "parsed", + "failed", + "unsupported" + ], + "title": "Parse Status", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "redacted_sha256": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Redacted Sha256" + }, + "resolved_through": { + "items": { + "type": "string" + }, + "title": "Resolved Through", + "type": "array" + }, + "scope": { + "enum": [ + "repository", + "local_static" + ], + "title": "Scope", + "type": "string" + } + }, + "required": [ + "artifact_id", + "host", + "scope", + "path", + "kind", + "parse_status" + ], + "title": "HostArtifactV8", + "type": "object" + }, + "HostCoverageV8": { + "additionalProperties": false, + "properties": { + "host": { + "enum": [ + "codex", + "claude-code", + "cursor", + "vscode", + "github", + "openshell" + ], + "title": "Host", + "type": "string" + }, + "issue_ids": { + "items": { + "type": "string" + }, + "title": "Issue Ids", + "type": "array" + }, + "scope": { + "enum": [ + "repository", + "local_static" + ], + "title": "Scope", + "type": "string" + }, + "sources_expected": { + "items": { + "type": "string" + }, + "title": "Sources Expected", + "type": "array" + }, + "sources_observed": { + "items": { + "type": "string" + }, + "title": "Sources Observed", + "type": "array" + }, + "status": { + "enum": [ + "complete", + "partial", + "experimental" + ], + "title": "Status", + "type": "string" + } + }, + "required": [ + "host", + "scope", + "status" + ], + "title": "HostCoverageV8", + "type": "object" + }, + "HostGrantsInventoryV8": { + "additionalProperties": false, + "properties": { + "artifacts": { + "items": { + "$ref": "#/$defs/HostArtifactV8" + }, + "title": "Artifacts", + "type": "array" + }, + "excluded_scopes": { + "items": { + "type": "string" + }, + "title": "Excluded Scopes", + "type": "array" + }, + "grants": { + "items": { + "discriminator": { + "mapping": { + "additional_path": "#/$defs/HostAdditionalPathGrantV2", + "hook": "#/$defs/HostHookGrantV7", + "instruction_trust_root": "#/$defs/HostInstructionGrantV2", + "mcp_server": "#/$defs/HostMcpServerGrantV7", + "openshell_policy": "#/$defs/HostOpenShellPolicyGrantV8", + "permission_mode": "#/$defs/HostPermissionModeGrantV2", + "permission_rule": "#/$defs/HostPermissionRuleGrantV2", + "plugin_or_app": "#/$defs/HostPluginGrantV2", + "profile": "#/$defs/HostProfileGrantV2", + "requirement": "#/$defs/HostRequirementGrantV2", + "sandbox": "#/$defs/HostSandboxGrantV2", + "workflow": "#/$defs/HostWorkflowGrantV7" + }, + "propertyName": "kind" + }, + "oneOf": [ + { + "$ref": "#/$defs/HostMcpServerGrantV7" + }, + { + "$ref": "#/$defs/HostPermissionRuleGrantV2" + }, + { + "$ref": "#/$defs/HostPermissionModeGrantV2" + }, + { + "$ref": "#/$defs/HostHookGrantV7" + }, + { + "$ref": "#/$defs/HostSandboxGrantV2" + }, + { + "$ref": "#/$defs/HostAdditionalPathGrantV2" + }, + { + "$ref": "#/$defs/HostPluginGrantV2" + }, + { + "$ref": "#/$defs/HostProfileGrantV2" + }, + { + "$ref": "#/$defs/HostRequirementGrantV2" + }, + { + "$ref": "#/$defs/HostWorkflowGrantV7" + }, + { + "$ref": "#/$defs/HostInstructionGrantV2" + }, + { + "$ref": "#/$defs/HostOpenShellPolicyGrantV8" + } + ] + }, + "title": "Grants", + "type": "array" + }, + "host_coverage": { + "items": { + "$ref": "#/$defs/HostCoverageV8" + }, + "title": "Host Coverage", + "type": "array" + }, + "host_grants_inventory_schema_version": { + "const": "0.8", + "default": "0.8", + "title": "Host Grants Inventory Schema Version", + "type": "string" + }, + "issues": { + "items": { + "$ref": "#/$defs/HostInventoryIssueV8" + }, + "title": "Issues", + "type": "array" + }, + "runtime_session_verified": { + "const": false, + "default": false, + "title": "Runtime Session Verified", + "type": "boolean" + }, + "scope": { + "default": "repository", + "enum": [ + "repository", + "local_static" + ], + "title": "Scope", + "type": "string" + }, + "static_analysis_only": { + "const": true, + "default": true, + "title": "Static Analysis Only", + "type": "boolean" + }, + "workspace": { + "title": "Workspace", + "type": "string" + } + }, + "required": [ + "workspace" + ], + "title": "HostGrantsInventoryV8", + "type": "object" + }, + "HostHookCommandV7": { + "additionalProperties": false, + "description": "A hook command as its grant publishes it: the executable's name and a digest of the whole command.\n\n``executable`` is the last path segment of the command's first\nwhitespace-separated word, when it is a plain token\n(``[A-Za-z0-9._+-]``, at most 80 characters) no redaction rule rewrites\nand the word is no shell reserved word and holds no ``://``, and\n```` otherwise, so no part of a URL is named. It\nis a label, not a claim about what a host runs. ``sha256`` is the digest of the whole command as\n``config_sha256``'s input holds it, so it moves only when that digest\ndoes; a value that input redacts moves neither. The command's text is\nnever published.", + "properties": { + "executable": { + "title": "Executable", + "type": "string" + }, + "sha256": { + "pattern": "^[0-9a-f]{64}$", + "title": "Sha256", + "type": "string" + } + }, + "required": [ + "executable", + "sha256" + ], + "title": "HostHookCommandV7", + "type": "object" + }, + "HostHookGrantV7": { + "additionalProperties": false, + "properties": { + "access": { + "enum": [ + "none", + "read", + "write", + "execute", + "external", + "admin", + "unknown" + ], + "title": "Access", + "type": "string" + }, + "config_sha256": { + "title": "Config Sha256", + "type": "string" + }, + "event": { + "title": "Event", + "type": "string" + }, + "grant_id": { + "title": "Grant Id", + "type": "string" + }, + "handlers": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/HostHookHandlerV7" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Handlers" + }, + "host": { + "enum": [ + "codex", + "claude-code", + "cursor", + "vscode", + "github" + ], + "title": "Host", + "type": "string" + }, + "kind": { + "const": "hook", + "default": "hook", + "title": "Kind", + "type": "string" + }, + "omitted_handlers": { + "default": 0, + "minimum": 0, + "title": "Omitted Handlers", + "type": "integer" + }, + "risk": { + "enum": [ + "none", + "low", + "medium", + "high", + "critical", + "unknown" + ], + "title": "Risk", + "type": "string" + }, + "scope": { + "enum": [ + "repository", + "local_static" + ], + "title": "Scope", + "type": "string" + }, + "script_inputs": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/HostHookScriptInputV7" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Script Inputs" + }, + "source": { + "title": "Source", + "type": "string" + } + }, + "required": [ + "grant_id", + "host", + "scope", + "source", + "config_sha256", + "access", + "risk", + "event", + "handlers" + ], + "title": "HostHookGrantV7", + "type": "object" + }, + "HostHookHandlerV7": { + "additionalProperties": false, + "description": "One hook handler under an event: its group's matcher, its command and its timeout.\n\n``matcher`` is ``None`` when its group declares none, which the host reads\nas every tool or source, and ```` when it is not a string or is\nlonger than 1,024 characters as ``config_sha256``'s input holds it;\notherwise it passes through the published-label redaction and is cut at\n120 characters. ``command`` is ``None`` for a handler with\nno command string, such as a ``prompt`` handler, whose prompt is not\npublished. ``timeout`` is the declared number or boolean; an integer of\nmore than 80 digits is published as its digits cut with ``\u2026``, a string\nas written when it is a plain token, and any other value, a non-finite\nfloat among them, as ````. Other handler settings are not\npublished; a change confined to them is a row whose text says it is not\nshown.", + "properties": { + "command": { + "anyOf": [ + { + "$ref": "#/$defs/HostHookCommandV7" + }, + { + "type": "null" + } + ], + "default": null + }, + "decision_limit": { + "anyOf": [ + { + "const": "script_or_command_behavior_not_read", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Decision Limit" + }, + "inline_allow": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Inline Allow" + }, + "matcher": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Matcher" + }, + "timeout": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "integer" + }, + { + "type": "number" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Timeout" + } + }, + "title": "HostHookHandlerV7", + "type": "object" + }, + "HostHookScriptInputV7": { + "additionalProperties": false, + "description": "A direct executable reference and its byte reading, never script semantics.", + "properties": { + "basis": { + "anyOf": [ + { + "enum": [ + "project_root_placeholder", + "plugin_root_placeholder", + "absolute_workspace_path" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Basis" + }, + "handler": { + "minimum": 0, + "title": "Handler", + "type": "integer" + }, + "limit": { + "anyOf": [ + { + "enum": [ + "unsupported_host", + "not_command_handler", + "unsupported_command_shape", + "platform_command_override", + "unsupported_shell", + "unsupported_exec_form", + "unsupported_shell_command", + "dynamic_command_argument", + "unexpanded_path_placeholder", + "unsupported_path_placeholder", + "plugin_root_not_established", + "unsupported_or_escaping_path", + "dynamic_or_conditional_path", + "working_directory_not_established", + "interpreter_wrapper", + "path_lookup", + "external_executable", + "unsupported_hook_shape", + "handler_bound_exceeded", + "hook_selection_not_established", + "redacted_dependency_path", + "escaping_path", + "missing_input", + "symlink_input", + "non_regular_input", + "oversized_input", + "unsafe_or_unreadable_input", + "unreadable_input" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Limit" + }, + "path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Path" + }, + "sha256": { + "anyOf": [ + { + "pattern": "^[0-9a-f]{64}$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Sha256" + }, + "size_bytes": { + "anyOf": [ + { + "minimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Size Bytes" + } + }, + "required": [ + "handler" + ], + "title": "HostHookScriptInputV7", + "type": "object" + }, + "HostInstructionGrantV2": { + "additionalProperties": false, + "properties": { + "access": { + "enum": [ + "none", + "read", + "write", + "execute", + "external", + "admin", + "unknown" + ], + "title": "Access", + "type": "string" + }, + "config_sha256": { + "title": "Config Sha256", + "type": "string" + }, + "grant_id": { + "title": "Grant Id", + "type": "string" + }, + "host": { + "enum": [ + "codex", + "claude-code", + "cursor", + "vscode", + "github" + ], + "title": "Host", + "type": "string" + }, + "kind": { + "const": "instruction_trust_root", + "default": "instruction_trust_root", + "title": "Kind", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "risk": { + "enum": [ + "none", + "low", + "medium", + "high", + "critical", + "unknown" + ], + "title": "Risk", + "type": "string" + }, + "scope": { + "enum": [ + "repository", + "local_static" + ], + "title": "Scope", + "type": "string" + }, + "source": { + "title": "Source", + "type": "string" + } + }, + "required": [ + "grant_id", + "host", + "scope", + "source", + "config_sha256", + "access", + "risk", + "path" + ], + "title": "HostInstructionGrantV2", + "type": "object" + }, + "HostInventoryIssueV8": { + "additionalProperties": false, + "properties": { + "blocking": { + "title": "Blocking", + "type": "boolean" + }, + "host": { + "enum": [ + "codex", + "claude-code", + "cursor", + "vscode", + "github", + "openshell" + ], + "title": "Host", + "type": "string" + }, + "issue_id": { + "title": "Issue Id", + "type": "string" + }, + "kind": { + "enum": [ + "parse_failed", + "unreadable", + "unsupported", + "unresolved_precedence", + "dynamic_source_excluded", + "remote_source_excluded" + ], + "title": "Kind", + "type": "string" + }, + "message": { + "title": "Message", + "type": "string" + }, + "source": { + "title": "Source", + "type": "string" + } + }, + "required": [ + "issue_id", + "kind", + "host", + "source", + "message", + "blocking" + ], + "title": "HostInventoryIssueV8", + "type": "object" + }, + "HostMcpLaunchSourceV7": { + "additionalProperties": false, + "properties": { + "package": { + "anyOf": [ + { + "maxLength": 200, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Package" + }, + "pin": { + "enum": [ + "pinned", + "mutable" + ], + "title": "Pin", + "type": "string" + } + }, + "required": [ + "pin" + ], + "title": "HostMcpLaunchSourceV7", + "type": "object" + }, + "HostMcpServerGrantV7": { + "additionalProperties": false, + "properties": { + "access": { + "enum": [ + "none", + "read", + "write", + "execute", + "external", + "admin", + "unknown" + ], + "title": "Access", + "type": "string" + }, + "args_sha256": { + "anyOf": [ + { + "pattern": "^[0-9a-f]{64}$", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Args Sha256" + }, + "config_sha256": { + "title": "Config Sha256", + "type": "string" + }, + "endpoint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Endpoint" + }, + "env_keys": { + "items": { + "type": "string" + }, + "title": "Env Keys", + "type": "array" + }, + "grant_id": { + "title": "Grant Id", + "type": "string" + }, + "header_keys": { + "items": { + "type": "string" + }, + "title": "Header Keys", + "type": "array" + }, + "host": { + "enum": [ + "codex", + "claude-code", + "cursor", + "vscode", + "github" + ], + "title": "Host", + "type": "string" + }, + "kind": { + "const": "mcp_server", + "default": "mcp_server", + "title": "Kind", + "type": "string" + }, + "launch_source": { + "anyOf": [ + { + "$ref": "#/$defs/HostMcpLaunchSourceV7" + }, + { + "type": "null" + } + ], + "default": null + }, + "package": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Package" + }, + "risk": { + "enum": [ + "none", + "low", + "medium", + "high", + "critical", + "unknown" + ], + "title": "Risk", + "type": "string" + }, + "scope": { + "enum": [ + "repository", + "local_static" + ], + "title": "Scope", + "type": "string" + }, + "server": { + "title": "Server", + "type": "string" + }, + "source": { + "title": "Source", + "type": "string" + }, + "transport": { + "title": "Transport", + "type": "string" + } + }, + "required": [ + "grant_id", + "host", + "scope", + "source", + "config_sha256", + "access", + "risk", + "server", + "transport", + "package", + "args_sha256" + ], + "title": "HostMcpServerGrantV7", + "type": "object" + }, + "HostOpenShellPolicyGrantV8": { + "additionalProperties": false, + "properties": { + "access": { + "enum": [ + "none", + "read", + "write", + "execute", + "external", + "admin", + "unknown" + ], + "title": "Access", + "type": "string" + }, + "config_sha256": { + "title": "Config Sha256", + "type": "string" + }, + "facts": { + "$ref": "#/$defs/OpenShellPolicyFacts" + }, + "grant_id": { + "title": "Grant Id", + "type": "string" + }, + "host": { + "const": "openshell", + "default": "openshell", + "title": "Host", + "type": "string" + }, + "kind": { + "const": "openshell_policy", + "default": "openshell_policy", + "title": "Kind", + "type": "string" + }, + "risk": { + "enum": [ + "none", + "low", + "medium", + "high", + "critical", + "unknown" + ], + "title": "Risk", + "type": "string" + }, + "scope": { + "enum": [ + "repository", + "local_static" + ], + "title": "Scope", + "type": "string" + }, + "source": { + "title": "Source", + "type": "string" + } + }, + "required": [ + "grant_id", + "scope", + "source", + "config_sha256", + "access", + "risk", + "facts" + ], + "title": "HostOpenShellPolicyGrantV8", + "type": "object" + }, + "HostPermissionModeGrantV2": { + "additionalProperties": false, + "properties": { + "access": { + "enum": [ + "none", + "read", + "write", + "execute", + "external", + "admin", + "unknown" + ], + "title": "Access", + "type": "string" + }, + "config_sha256": { + "title": "Config Sha256", + "type": "string" + }, + "grant_id": { + "title": "Grant Id", + "type": "string" + }, + "host": { + "enum": [ + "codex", + "claude-code", + "cursor", + "vscode", + "github" + ], + "title": "Host", + "type": "string" + }, + "kind": { + "const": "permission_mode", + "default": "permission_mode", + "title": "Kind", + "type": "string" + }, + "risk": { + "enum": [ + "none", + "low", + "medium", + "high", + "critical", + "unknown" + ], + "title": "Risk", + "type": "string" + }, + "scope": { + "enum": [ + "repository", + "local_static" + ], + "title": "Scope", + "type": "string" + }, + "setting": { + "title": "Setting", + "type": "string" + }, + "source": { + "title": "Source", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "grant_id", + "host", + "scope", + "source", + "config_sha256", + "access", + "risk", + "setting", + "value" + ], + "title": "HostPermissionModeGrantV2", + "type": "object" + }, + "HostPermissionRuleGrantV2": { + "additionalProperties": false, + "properties": { + "access": { + "enum": [ + "none", + "read", + "write", + "execute", + "external", + "admin", + "unknown" + ], + "title": "Access", + "type": "string" + }, + "config_sha256": { + "title": "Config Sha256", + "type": "string" + }, + "disposition": { + "enum": [ + "allow", + "ask", + "deny" + ], + "title": "Disposition", + "type": "string" + }, + "grant_id": { + "title": "Grant Id", + "type": "string" + }, + "host": { + "enum": [ + "codex", + "claude-code", + "cursor", + "vscode", + "github" + ], + "title": "Host", + "type": "string" + }, + "kind": { + "const": "permission_rule", + "default": "permission_rule", + "title": "Kind", + "type": "string" + }, + "risk": { + "enum": [ + "none", + "low", + "medium", + "high", + "critical", + "unknown" + ], + "title": "Risk", + "type": "string" + }, + "rule": { + "title": "Rule", + "type": "string" + }, + "scope": { + "enum": [ + "repository", + "local_static" + ], + "title": "Scope", + "type": "string" + }, + "source": { + "title": "Source", + "type": "string" + }, + "wildcard": { + "default": false, + "title": "Wildcard", + "type": "boolean" + } + }, + "required": [ + "grant_id", + "host", + "scope", + "source", + "config_sha256", + "access", + "risk", + "disposition", + "rule" + ], + "title": "HostPermissionRuleGrantV2", + "type": "object" + }, + "HostPluginGrantV2": { + "additionalProperties": false, + "properties": { + "access": { + "enum": [ + "none", + "read", + "write", + "execute", + "external", + "admin", + "unknown" + ], + "title": "Access", + "type": "string" + }, + "config_sha256": { + "title": "Config Sha256", + "type": "string" + }, + "enabled": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Enabled" + }, + "grant_id": { + "title": "Grant Id", + "type": "string" + }, + "host": { + "enum": [ + "codex", + "claude-code", + "cursor", + "vscode", + "github" + ], + "title": "Host", + "type": "string" + }, + "kind": { + "const": "plugin_or_app", + "default": "plugin_or_app", + "title": "Kind", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "risk": { + "enum": [ + "none", + "low", + "medium", + "high", + "critical", + "unknown" + ], + "title": "Risk", + "type": "string" + }, + "scope": { + "enum": [ + "repository", + "local_static" + ], + "title": "Scope", + "type": "string" + }, + "source": { + "title": "Source", + "type": "string" + } + }, + "required": [ + "grant_id", + "host", + "scope", + "source", + "config_sha256", + "access", + "risk", + "name" + ], + "title": "HostPluginGrantV2", + "type": "object" + }, + "HostProfileGrantV2": { + "additionalProperties": false, + "properties": { + "access": { + "enum": [ + "none", + "read", + "write", + "execute", + "external", + "admin", + "unknown" + ], + "title": "Access", + "type": "string" + }, + "config_sha256": { + "title": "Config Sha256", + "type": "string" + }, + "grant_id": { + "title": "Grant Id", + "type": "string" + }, + "host": { + "enum": [ + "codex", + "claude-code", + "cursor", + "vscode", + "github" + ], + "title": "Host", + "type": "string" + }, + "kind": { + "const": "profile", + "default": "profile", + "title": "Kind", + "type": "string" + }, + "profile": { + "title": "Profile", + "type": "string" + }, + "resolved": { + "title": "Resolved", + "type": "boolean" + }, + "risk": { + "enum": [ + "none", + "low", + "medium", + "high", + "critical", + "unknown" + ], + "title": "Risk", + "type": "string" + }, + "scope": { + "enum": [ + "repository", + "local_static" + ], + "title": "Scope", + "type": "string" + }, + "source": { + "title": "Source", + "type": "string" + } + }, + "required": [ + "grant_id", + "host", + "scope", + "source", + "config_sha256", + "access", + "risk", + "profile", + "resolved" + ], + "title": "HostProfileGrantV2", + "type": "object" + }, + "HostRequirementGrantV2": { + "additionalProperties": false, + "properties": { + "access": { + "enum": [ + "none", + "read", + "write", + "execute", + "external", + "admin", + "unknown" + ], + "title": "Access", + "type": "string" + }, + "config_sha256": { + "title": "Config Sha256", + "type": "string" + }, + "grant_id": { + "title": "Grant Id", + "type": "string" + }, + "host": { + "enum": [ + "codex", + "claude-code", + "cursor", + "vscode", + "github" + ], + "title": "Host", + "type": "string" + }, + "kind": { + "const": "requirement", + "default": "requirement", + "title": "Kind", + "type": "string" + }, + "requirement": { + "title": "Requirement", + "type": "string" + }, + "risk": { + "enum": [ + "none", + "low", + "medium", + "high", + "critical", + "unknown" + ], + "title": "Risk", + "type": "string" + }, + "scope": { + "enum": [ + "repository", + "local_static" + ], + "title": "Scope", + "type": "string" + }, + "source": { + "title": "Source", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "grant_id", + "host", + "scope", + "source", + "config_sha256", + "access", + "risk", + "requirement", + "value" + ], + "title": "HostRequirementGrantV2", + "type": "object" + }, + "HostReusableWorkflowCallV6": { + "additionalProperties": false, + "properties": { + "job": { + "title": "Job", + "type": "string" + }, + "secret_mappings": { + "items": { + "$ref": "#/$defs/HostReusableWorkflowSecretV6" + }, + "title": "Secret Mappings", + "type": "array" + }, + "secrets_inherit": { + "title": "Secrets Inherit", + "type": "boolean" + }, + "uses": { + "title": "Uses", + "type": "string" + }, + "uses_redacted": { + "default": false, + "title": "Uses Redacted", + "type": "boolean" + } + }, + "required": [ + "job", + "uses", + "secrets_inherit" + ], + "title": "HostReusableWorkflowCallV6", + "type": "object" + }, + "HostReusableWorkflowSecretV6": { + "additionalProperties": false, + "description": "One named secret a job passes to the reusable workflow it calls (#693).\n\n``destination`` is the callee's secret input name as the caller writes it.\n``source`` is ``NAME`` from a whole-value ``${{ secrets.NAME }}``, and\n``form`` is then ``secret``. The name is a reference, never a value: it\ndoes not establish the secret's privilege, whether the caller has it, or\nwhat the called workflow does with it. Anything else is ``unresolved``,\nand none of its value is published or digested: a literal value, any\nother expression, a non-string, or a ``secrets`` that is neither\n``inherit`` nor a mapping (``destination`` is then ``null``). A name the\ncredential redactors rewrite is ``redacted`` and records a blocking\ncoverage issue, because two values that redact alike must never compare as\nunchanged. Every other unresolved mapping records a non-blocking one naming\nits ``job/destination``: only that value is uncompared, so the rest of the\nfile still compares.", + "properties": { + "destination": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Destination" + }, + "form": { + "enum": [ + "secret", + "unresolved" + ], + "title": "Form", + "type": "string" + }, + "source": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source" + }, + "unresolved_reason": { + "anyOf": [ + { + "enum": [ + "literal_value", + "expression", + "not_a_string", + "redacted", + "secrets_not_a_mapping" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Unresolved Reason" + } + }, + "required": [ + "destination", + "source", + "form" + ], + "title": "HostReusableWorkflowSecretV6", + "type": "object" + }, + "HostSandboxGrantV2": { + "additionalProperties": false, + "properties": { + "access": { + "enum": [ + "none", + "read", + "write", + "execute", + "external", + "admin", + "unknown" + ], + "title": "Access", + "type": "string" + }, + "config_sha256": { + "title": "Config Sha256", + "type": "string" + }, + "grant_id": { + "title": "Grant Id", + "type": "string" + }, + "host": { + "enum": [ + "codex", + "claude-code", + "cursor", + "vscode", + "github" + ], + "title": "Host", + "type": "string" + }, + "kind": { + "const": "sandbox", + "default": "sandbox", + "title": "Kind", + "type": "string" + }, + "risk": { + "enum": [ + "none", + "low", + "medium", + "high", + "critical", + "unknown" + ], + "title": "Risk", + "type": "string" + }, + "scope": { + "enum": [ + "repository", + "local_static" + ], + "title": "Scope", + "type": "string" + }, + "setting": { + "title": "Setting", + "type": "string" + }, + "source": { + "title": "Source", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "grant_id", + "host", + "scope", + "source", + "config_sha256", + "access", + "risk", + "setting", + "value" + ], + "title": "HostSandboxGrantV2", + "type": "object" + }, + "HostWorkflowAgentLaunchV7": { + "additionalProperties": false, + "description": "A step that launches a known coding agent, read as text and never run (#823).\n\n``agent`` is a documented action reference's ``owner/repo`` (the step's\n``uses:`` at any ref; the Claude base action also as the ``base-action``\ndirectory of ``anthropics/claude-code-action``), or a known agent CLI a\n``run:`` launches when the whole ``run:`` is one line of plain words\n(letters, digits and ``_ . / : = , % + -``, separated by spaces or tabs),\nrun by ``bash``, ``sh`` or the runner's default shell, whose program,\nafter any ``NAME=value`` assignments, has the file name ``claude`` and\npasses ``-p``/``--print``, or ``codex`` followed by ``exec`` (``e``).\n``form: read`` lists the documented permission inputs or flags the step\ndeclares in ``settings``, and the documented widening rules they meet in\n``widening_rules``, omitted when none. ``form: unresolved`` is an agent\naction whose ``with:`` is not a mapping (``inputs_not_a_mapping``), with\nno settings, and records a non-blocking coverage issue. Any other\n``run:`` that mentions an agent CLI is not a launch: it is listed in\n``unread_agent_runs``. ``job_secrets`` names the secrets the step's job\nreferences (``${{ secrets.NAME }}``) and the workflow-level ``env``\npasses: context for the row that names this step, never compared.\n``job`` and ``step`` are published labels (#802).", + "properties": { + "agent": { + "enum": [ + "anthropics/claude-code-action", + "anthropics/claude-code-base-action", + "anthropics/claude-code-action/base-action", + "openai/codex-action", + "claude", + "codex" + ], + "title": "Agent", + "type": "string" + }, + "form": { + "enum": [ + "read", + "unresolved" + ], + "title": "Form", + "type": "string" + }, + "job": { + "title": "Job", + "type": "string" + }, + "job_secrets": { + "items": { + "type": "string" + }, + "title": "Job Secrets", + "type": "array" + }, + "settings": { + "items": { + "$ref": "#/$defs/HostWorkflowAgentSettingV7" + }, + "title": "Settings", + "type": "array" + }, + "step": { + "title": "Step", + "type": "string" + }, + "unresolved_reason": { + "anyOf": [ + { + "const": "inputs_not_a_mapping", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Unresolved Reason" + }, + "widening_rules": { + "items": { + "$ref": "#/$defs/HostWorkflowAgentRuleV7" + }, + "title": "Widening Rules", + "type": "array" + } + }, + "required": [ + "job", + "step", + "agent", + "form" + ], + "title": "HostWorkflowAgentLaunchV7", + "type": "object" + }, + "HostWorkflowAgentRuleV7": { + "additionalProperties": false, + "description": "One documented widening rule an agent launch meets, and the setting it was read from (#823).\n\nDecided when the workflow is read, from the declared text, before any of\nit is withheld for publication, so redaction never hides a rule. Only\ntext this reader reads exactly meets one: ``claude_args`` or\n``codex-args`` only when it is a plain list of words (never when it holds\na ``${{ }}`` expression), the entries of a user gate that hold no\nexpression, and a mode or ``settings`` input that holds none. Claude Code\nsettings written as JSON in the ``settings`` input meet\n``bypass_permissions`` when their ``defaultMode`` is\n``bypassPermissions``, read as the settings reader reads it; a path to a\nsettings file is not read. ``setting`` is the input (``claude_args``,\n``allowed_bots``, ``sandbox``, ``permission-profile``, \u2026) or the CLI\nflag's primary spelling. One rule compares as one whatever setting meets\nit, except ``open_gate``, which is one rule per gate input.", + "properties": { + "rule": { + "enum": [ + "bypass_permissions", + "bypass_approvals_and_sandbox", + "danger_full_access", + "unsafe_safety_strategy", + "open_gate" + ], + "title": "Rule", + "type": "string" + }, + "setting": { + "title": "Setting", + "type": "string" + } + }, + "required": [ + "rule", + "setting" + ], + "title": "HostWorkflowAgentRuleV7", + "type": "object" + }, + "HostWorkflowAgentSettingV7": { + "additionalProperties": false, + "description": "One permission input or flag an agent launch declares, compared as text (#823).\n\n``name`` is the documented input (``claude_args``, ``sandbox``, \u2026) or the\nflag's primary spelling (``--allowedTools`` for ``--allowed-tools`` too).\n``value`` is the declared text, stripped, as it may be published; a flag\nthat takes no value has ``null``.\n\n``claude_args`` and ``codex-args`` are read only when they are a plain\nlist of words: letters, digits and ``_ . / : = , % + - ( )``, separated by\nblanks or newlines, with no ``--settings`` or ``--mcp-config`` flag. Every\nparser involved splits such text the same way, so it is published as\nthose words, one space apart. Any other value \u2014 holding a quote, a\n``${{ }}`` expression, ``$``, a backtick, a comment, a shell operator,\nJSON or another character \u2014 is ``unread_arguments``: ``value`` is\n````, a short digest, so an edit to it is still a change\nwhile none of its text is published; no documented widening rule is read\nfrom it; and it records a non-blocking coverage issue naming its\n``job/step`` (#823 review cycle 4). A codex ``--config`` override keeps\nits key; its value is ```` under ``env``, ``headers`` or a\nsecret-named key, as the host readers redact such values, published as\nwritten for ``sandbox_mode``, ``default_permissions``,\n``approval_policy`` and ``model``, and ```` otherwise.\n\nEvery other input is one value. A JSON object (a ``settings`` or\n``mcp_config`` value) publishes its shape and none of its free text: key\nnames, numbers, booleans and ``null``, with each string replaced by\n````, a short digest of what the host readers digest for it,\nso an edit to it is still a change. ``env`` and ``headers`` values,\n``apiKeyHelper`` and every secret-named value are ````, as the\nhost readers redact them. The strings a host reader publishes are kept:\na ``permissions.allow``/``ask``/``deny`` rule and a documented Claude\nCode setting's value such as ``defaultMode``, and an MCP server's command\nname and its URL's scheme and host, each followed by the digest when it\ndrops something the digest reads (a command's arguments, a URL's query).\nSo an MCP server's arguments and a hook's command publish nothing, as\n`.mcp.json` and `.claude/settings.json` do not (#823 review). A\n``settings`` or ``mcp_config`` value that neither starts like a JSON\nobject nor is a plain file path (path characters, and a ``${{ }}``\nexpression only as a plain context reference) is ````, a\ndigest and none of its text (#823 review cycle 5). A URL in\nother text publishes its scheme and host with ```` for its\npath and query (#723). Other text \u2014 a prompt, a flag's value \u2014 is\npublished through the workflow label redaction (#802). A value it\nrewrites is credential-shaped \u2014 a token, but also prose such as \"never\nprint bearer tokens\" \u2014 and is published redacted with\n``unresolved_reason: redacted``: it is compared as published, beside the\nrules read from its declared text, and records a non-blocking coverage\nissue naming its ``job/step``, because an edit inside what is redacted is\nnot reported. A value that is not a string (``not_a_string``), or one\nholding text that starts like JSON and does not parse (``unparsed_json``),\nis ``null`` and records a non-blocking coverage issue naming its\n``job/step``: it is neither published nor compared.\n\n``holds_expression`` is ``true`` when an input other than an argument\ninput holds a ``${{ }}`` expression, which GitHub substitutes before the\naction reads the input, and is omitted otherwise. A documented widening\nrule is then read only from the entries of a user gate that hold none,\nand from no mode or settings input, and a rule the launch gains in the\nsame job afterwards is not claimed, because the substituted text may\nalready have met it.", + "properties": { + "holds_expression": { + "default": false, + "title": "Holds Expression", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "unresolved_reason": { + "anyOf": [ + { + "enum": [ + "not_a_string", + "redacted", + "unparsed_json", + "unread_arguments" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Unresolved Reason" + }, + "value": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Value" + } + }, + "required": [ + "name", + "value" + ], + "title": "HostWorkflowAgentSettingV7", + "type": "object" + }, + "HostWorkflowCheckoutRefV7": { + "additionalProperties": false, + "description": "One ``actions/checkout`` step and the ``with.ref`` it declares, as text (#823).\n\n``ref`` is ``null`` when the step declares none, or an empty one: the\ncheckout's default for the triggering event. A ref the label redaction\nrewrites is published redacted with ``unresolved_reason: redacted`` and\nmakes the workflow a blocking limit, as a redacted step reference does\n(#767): a ref names the code the job runs, as a step reference does. A\nvalue that is not a string, or ``with:`` that is not a mapping,\nis ``null`` with ``unresolved_reason`` and records a non-blocking coverage\nissue. The ref is never resolved or fetched.", + "properties": { + "job": { + "title": "Job", + "type": "string" + }, + "ref": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Ref" + }, + "step": { + "title": "Step", + "type": "string" + }, + "unresolved_reason": { + "anyOf": [ + { + "enum": [ + "not_a_string", + "redacted", + "inputs_not_a_mapping" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Unresolved Reason" + } + }, + "required": [ + "job", + "step", + "ref" + ], + "title": "HostWorkflowCheckoutRefV7", + "type": "object" + }, + "HostWorkflowGrantV7": { + "additionalProperties": false, + "description": "A v0.6 workflow grant plus the agent launches, unread agent steps and checkout refs its steps declare.\n\nEach list is present only when a step declares one. In a v0.7 grant an\nabsent list means the steps were read and declare none; the schema\nversion, not the key, separates that from a legacy grant that never read\nthem. ``unread_agent_runs`` is a named limit and is never compared.\n``access`` and ``risk`` still describe the workflow's token and triggers\nalone.", + "properties": { + "access": { + "enum": [ + "none", + "read", + "write", + "execute", + "external", + "admin", + "unknown" + ], + "title": "Access", + "type": "string" + }, + "agent_launches": { + "items": { + "$ref": "#/$defs/HostWorkflowAgentLaunchV7" + }, + "title": "Agent Launches", + "type": "array" + }, + "checkout_refs": { + "items": { + "$ref": "#/$defs/HostWorkflowCheckoutRefV7" + }, + "title": "Checkout Refs", + "type": "array" + }, + "config_sha256": { + "title": "Config Sha256", + "type": "string" + }, + "effective_write_scopes": { + "items": { + "type": "string" + }, + "title": "Effective Write Scopes", + "type": "array" + }, + "grant_id": { + "title": "Grant Id", + "type": "string" + }, + "host": { + "enum": [ + "codex", + "claude-code", + "cursor", + "vscode", + "github" + ], + "title": "Host", + "type": "string" + }, + "kind": { + "const": "workflow", + "default": "workflow", + "title": "Kind", + "type": "string" + }, + "permission_contexts": { + "items": { + "$ref": "#/$defs/HostWorkflowPermissionsV4" + }, + "title": "Permission Contexts", + "type": "array" + }, + "pull_request_target": { + "default": false, + "title": "Pull Request Target", + "type": "boolean" + }, + "reusable_calls": { + "items": { + "$ref": "#/$defs/HostReusableWorkflowCallV6" + }, + "title": "Reusable Calls", + "type": "array" + }, + "risk": { + "enum": [ + "none", + "low", + "medium", + "high", + "critical", + "unknown" + ], + "title": "Risk", + "type": "string" + }, + "scope": { + "enum": [ + "repository", + "local_static" + ], + "title": "Scope", + "type": "string" + }, + "source": { + "title": "Source", + "type": "string" + }, + "step_actions": { + "items": { + "$ref": "#/$defs/HostWorkflowStepActionV6" + }, + "title": "Step Actions", + "type": "array" + }, + "triggers": { + "items": { + "type": "string" + }, + "title": "Triggers", + "type": "array" + }, + "unread_agent_runs": { + "items": { + "$ref": "#/$defs/HostWorkflowUnreadAgentRunV7" + }, + "title": "Unread Agent Runs", + "type": "array" + }, + "write_all": { + "default": false, + "title": "Write All", + "type": "boolean" + }, + "write_scopes": { + "items": { + "type": "string" + }, + "title": "Write Scopes", + "type": "array" + } + }, + "required": [ + "grant_id", + "host", + "scope", + "source", + "config_sha256", + "access", + "risk", + "permission_contexts", + "effective_write_scopes", + "reusable_calls" + ], + "title": "HostWorkflowGrantV7", + "type": "object" + }, + "HostWorkflowPermissionsV4": { + "additionalProperties": false, + "properties": { + "job": { + "title": "Job", + "type": "string" + }, + "permissions": { + "additionalProperties": { + "enum": [ + "read", + "write" + ], + "type": "string" + }, + "title": "Permissions", + "type": "object" + }, + "state": { + "enum": [ + "explicit", + "repository_default", + "unresolved" + ], + "title": "State", + "type": "string" + } + }, + "required": [ + "job", + "state", + "permissions" + ], + "title": "HostWorkflowPermissionsV4", + "type": "object" + }, + "HostWorkflowStepActionV6": { + "additionalProperties": false, + "description": "One step's declared action reference, read as text and never fetched.\n\n``form`` is ``remote`` for ``owner/repo[/path]@ref``, ``docker`` for\n``docker://\u2026``, and ``unresolved`` for a value Shipgate does not resolve\nto an action identity; ``unresolved_reason`` then says which. A job whose\n``steps`` is not a list, or a step that is not a mapping, is listed as\nunresolved too, with no ``uses``, so an absent list still means the steps\nwere read and declare nothing. A local\n``./\u2026`` reference is not listed: composite actions remain unread (#701).\n``step`` is the step's ``id``, else its ``name``, else ``steps[N]`` \u2014 the\nevidence a reviewer uses to find it, not part of the comparison. ``job``\nand ``step`` are published labels: credential-shaped text in either, and\nthe userinfo of any ``scheme://\u2026@`` inside it, is redacted (#802).", + "properties": { + "form": { + "enum": [ + "remote", + "docker", + "unresolved" + ], + "title": "Form", + "type": "string" + }, + "job": { + "title": "Job", + "type": "string" + }, + "step": { + "title": "Step", + "type": "string" + }, + "unresolved_reason": { + "anyOf": [ + { + "enum": [ + "expression", + "unsupported_reference", + "not_a_string", + "redacted", + "steps_not_a_list", + "step_not_a_mapping" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Unresolved Reason" + }, + "uses": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Uses" + } + }, + "required": [ + "job", + "step", + "uses", + "form" + ], + "title": "HostWorkflowStepActionV6", + "type": "object" + }, + "HostWorkflowUnreadAgentRunV7": { + "additionalProperties": false, + "description": "A ``run:`` step that mentions a known agent CLI and is not read as an agent launch (#823 review cycle 4).\n\nAny ``run:`` holding ``claude`` or ``codex`` as a word of its own that is\nnot an agent launch this reader reads \u2014 more than one line or command, a\nquote, an expansion, a redirection, a comment, a continuation, a\n``${{ }}`` expression, another program such as ``npx`` or ``timeout``, a\nsubcommand that is not a headless launch, or a declared ``shell:`` other\nthan ``bash`` or ``sh`` run on the script alone (so ``bash -c '\u2026' {0}``\ntoo) \u2014 once for each agent CLI it mentions. It is a\nnamed, non-blocking limit and nothing more: none of the step's text is\npublished, it is never compared, so adding, removing or editing it gives\nno row, and it never says that the step starts, or does not start, an\nagent. ``job`` and ``step`` are published labels (#802).", + "properties": { + "agent": { + "enum": [ + "claude", + "codex" + ], + "title": "Agent", + "type": "string" + }, + "job": { + "title": "Job", + "type": "string" + }, + "step": { + "title": "Step", + "type": "string" + } + }, + "required": [ + "job", + "step", + "agent" + ], + "title": "HostWorkflowUnreadAgentRunV7", + "type": "object" + }, + "InstructionStructureEvidence": { + "additionalProperties": false, + "properties": { + "profile": { + "title": "Profile", + "type": "string" + }, + "reason": { + "title": "Reason", + "type": "string" + }, + "sha256": { + "anyOf": [ + { + "pattern": "^sha256:[0-9a-f]{64}$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Sha256" + }, + "status": { + "enum": [ + "guidance", + "structured", + "unresolved" + ], + "title": "Status", + "type": "string" + } + }, + "required": [ + "profile", + "status", + "reason" + ], + "title": "InstructionStructureEvidence", + "type": "object" + }, + "OpenShellAllowRule": { + "additionalProperties": false, + "properties": { + "allow": { + "$ref": "#/$defs/OpenShellRequestMatcher" + } + }, + "required": [ + "allow" + ], + "title": "OpenShellAllowRule", + "type": "object" + }, + "OpenShellAnyMatcher": { + "additionalProperties": false, + "properties": { + "any": { + "items": { + "type": "string" + }, + "minItems": 1, + "title": "Any", + "type": "array" + } + }, + "required": [ + "any" + ], + "title": "OpenShellAnyMatcher", + "type": "object" + }, + "OpenShellBinary": { + "additionalProperties": false, + "properties": { + "path": { + "title": "Path", + "type": "string" + } + }, + "required": [ + "path" + ], + "title": "OpenShellBinary", + "type": "object" + }, + "OpenShellCredentialBinding": { + "additionalProperties": false, + "properties": { + "provider": { + "title": "Provider", + "type": "string" + } + }, + "required": [ + "provider" + ], + "title": "OpenShellCredentialBinding", + "type": "object" + }, + "OpenShellEndpoint": { + "additionalProperties": false, + "properties": { + "access": { + "default": "", + "enum": [ + "", + "read-only", + "read-write", + "full" + ], + "title": "Access", + "type": "string" + }, + "allow_encoded_slash": { + "default": false, + "title": "Allow Encoded Slash", + "type": "boolean" + }, + "allow_uninspected_credentials": { + "default": false, + "title": "Allow Uninspected Credentials", + "type": "boolean" + }, + "allowed_ips": { + "items": { + "type": "string" + }, + "title": "Allowed Ips", + "type": "array" + }, + "credential_binding": { + "anyOf": [ + { + "$ref": "#/$defs/OpenShellCredentialBinding" + }, + { + "type": "null" + } + ], + "default": null + }, + "credential_signing": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Credential Signing" + }, + "deny_rules": { + "items": { + "$ref": "#/$defs/OpenShellRequestMatcher" + }, + "title": "Deny Rules", + "type": "array" + }, + "enforcement": { + "default": "audit", + "enum": [ + "audit", + "enforce", + "" + ], + "title": "Enforcement", + "type": "string" + }, + "graphql_max_body_bytes": { + "default": 65536, + "maximum": 4294967295, + "minimum": 0, + "title": "Graphql Max Body Bytes", + "type": "integer" + }, + "graphql_persisted_queries": { + "additionalProperties": { + "$ref": "#/$defs/OpenShellGraphqlOperation" + }, + "title": "Graphql Persisted Queries", + "type": "object" + }, + "host": { + "default": "", + "title": "Host", + "type": "string" + }, + "json_rpc": { + "anyOf": [ + { + "$ref": "#/$defs/OpenShellJsonRpcOptions" + }, + { + "type": "null" + } + ], + "default": null + }, + "mcp": { + "anyOf": [ + { + "$ref": "#/$defs/OpenShellMcpOptions" + }, + { + "type": "null" + } + ], + "default": null + }, + "path": { + "default": "", + "title": "Path", + "type": "string" + }, + "persisted_queries": { + "default": "deny", + "enum": [ + "", + "deny", + "allow_registered" + ], + "title": "Persisted Queries", + "type": "string" + }, + "port": { + "default": 0, + "maximum": 65535, + "minimum": 0, + "title": "Port", + "type": "integer" + }, + "ports": { + "items": { + "type": "integer" + }, + "title": "Ports", + "type": "array" + }, + "protocol": { + "default": "", + "enum": [ + "", + "rest", + "websocket", + "graphql", + "mcp", + "json-rpc", + "tcp" + ], + "title": "Protocol", + "type": "string" + }, + "request_body_credential_rewrite": { + "default": false, + "title": "Request Body Credential Rewrite", + "type": "boolean" + }, + "rules": { + "items": { + "$ref": "#/$defs/OpenShellAllowRule" + }, + "title": "Rules", + "type": "array" + }, + "signing_region": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Signing Region" + }, + "signing_service": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Signing Service" + }, + "tls": { + "default": "", + "enum": [ + "", + "skip" + ], + "title": "Tls", + "type": "string" + }, + "websocket_credential_rewrite": { + "default": false, + "title": "Websocket Credential Rewrite", + "type": "boolean" + } + }, + "title": "OpenShellEndpoint", + "type": "object" + }, + "OpenShellFilesystem": { + "additionalProperties": false, + "properties": { + "include_workdir": { + "default": false, + "title": "Include Workdir", + "type": "boolean" + }, + "read_only": { + "items": { + "type": "string" + }, + "title": "Read Only", + "type": "array" + }, + "read_write": { + "items": { + "type": "string" + }, + "title": "Read Write", + "type": "array" + } + }, + "title": "OpenShellFilesystem", + "type": "object" + }, + "OpenShellGraphqlOperation": { + "additionalProperties": false, + "properties": { + "fields": { + "items": { + "type": "string" + }, + "title": "Fields", + "type": "array" + }, + "operation_name": { + "default": "", + "title": "Operation Name", + "type": "string" + }, + "operation_type": { + "default": "", + "title": "Operation Type", + "type": "string" + } + }, + "title": "OpenShellGraphqlOperation", + "type": "object" + }, + "OpenShellJsonRpcOptions": { + "additionalProperties": false, + "properties": { + "max_body_bytes": { + "default": 65536, + "maximum": 4294967295, + "minimum": 0, + "title": "Max Body Bytes", + "type": "integer" + } + }, + "title": "OpenShellJsonRpcOptions", + "type": "object" + }, + "OpenShellLandlock": { + "additionalProperties": false, + "properties": { + "compatibility": { + "default": "best_effort", + "enum": [ + "best_effort", + "hard_requirement" + ], + "title": "Compatibility", + "type": "string" + } + }, + "title": "OpenShellLandlock", + "type": "object" + }, + "OpenShellMcpOptions": { + "additionalProperties": false, + "properties": { + "allow_all_known_mcp_methods": { + "default": false, + "title": "Allow All Known Mcp Methods", + "type": "boolean" + }, + "max_body_bytes": { + "default": 65536, + "maximum": 4294967295, + "minimum": 0, + "title": "Max Body Bytes", + "type": "integer" + }, + "strict_tool_names": { + "default": true, + "title": "Strict Tool Names", + "type": "boolean" + }, + "versions": { + "items": { + "type": "string" + }, + "title": "Versions", + "type": "array" + } + }, + "title": "OpenShellMcpOptions", + "type": "object" + }, + "OpenShellNetworkRule": { + "additionalProperties": false, + "properties": { + "binaries": { + "items": { + "$ref": "#/$defs/OpenShellBinary" + }, + "title": "Binaries", + "type": "array" + }, + "endpoints": { + "items": { + "$ref": "#/$defs/OpenShellEndpoint" + }, + "title": "Endpoints", + "type": "array" + }, + "name": { + "default": "", + "title": "Name", + "type": "string" + } + }, + "title": "OpenShellNetworkRule", + "type": "object" + }, + "OpenShellPolicy": { + "additionalProperties": false, + "properties": { + "filesystem_policy": { + "anyOf": [ + { + "$ref": "#/$defs/OpenShellFilesystem" + }, + { + "type": "null" + } + ], + "default": null + }, + "landlock": { + "anyOf": [ + { + "$ref": "#/$defs/OpenShellLandlock" + }, + { + "type": "null" + } + ], + "default": null + }, + "network_middlewares": { + "additionalProperties": true, + "title": "Network Middlewares", + "type": "object" + }, + "network_policies": { + "additionalProperties": { + "$ref": "#/$defs/OpenShellNetworkRule" + }, + "title": "Network Policies", + "type": "object" + }, + "process": { + "anyOf": [ + { + "$ref": "#/$defs/OpenShellProcess" + }, + { + "type": "null" + } + ], + "default": null + }, + "version": { + "const": 1, + "title": "Version", + "type": "integer" + } + }, + "required": [ + "version" + ], + "title": "OpenShellPolicy", + "type": "object" + }, + "OpenShellPolicyFacts": { + "additionalProperties": false, + "properties": { + "defaulted_fields": { + "items": { + "type": "string" + }, + "title": "Defaulted Fields", + "type": "array" + }, + "field_paths": { + "items": { + "type": "string" + }, + "title": "Field Paths", + "type": "array" + }, + "filesystem_baseline": { + "const": "runtime_dependent_not_resolved", + "default": "runtime_dependent_not_resolved", + "title": "Filesystem Baseline", + "type": "string" + }, + "include_workdir_when_filesystem_omitted": { + "const": true, + "default": true, + "title": "Include Workdir When Filesystem Omitted", + "type": "boolean" + }, + "landlock_when_omitted": { + "const": "best_effort", + "default": "best_effort", + "title": "Landlock When Omitted", + "type": "string" + }, + "policy": { + "$ref": "#/$defs/OpenShellPolicy" + }, + "policy_schema_version": { + "const": 1, + "default": 1, + "title": "Policy Schema Version", + "type": "integer" + }, + "process_omission": { + "const": "driver_default", + "default": "driver_default", + "title": "Process Omission", + "type": "string" + }, + "registration": { + "title": "Registration", + "type": "string" + }, + "role": { + "enum": [ + "authored", + "effective_snapshot" + ], + "title": "Role", + "type": "string" + }, + "runtime_freshness_verified": { + "const": false, + "default": false, + "title": "Runtime Freshness Verified", + "type": "boolean" + }, + "runtime_version": { + "const": "0.1.2", + "title": "Runtime Version", + "type": "string" + } + }, + "required": [ + "registration", + "role", + "runtime_version", + "policy" + ], + "title": "OpenShellPolicyFacts", + "type": "object" + }, + "OpenShellProcess": { + "additionalProperties": false, + "properties": { + "run_as_group": { + "default": "", + "title": "Run As Group", + "type": "string" + }, + "run_as_user": { + "default": "", + "title": "Run As User", + "type": "string" + } + }, + "title": "OpenShellProcess", + "type": "object" + }, + "OpenShellRequestMatcher": { + "additionalProperties": false, + "properties": { + "command": { + "default": "", + "title": "Command", + "type": "string" + }, + "fields": { + "items": { + "type": "string" + }, + "title": "Fields", + "type": "array" + }, + "method": { + "default": "", + "title": "Method", + "type": "string" + }, + "operation_name": { + "default": "", + "title": "Operation Name", + "type": "string" + }, + "operation_type": { + "default": "", + "title": "Operation Type", + "type": "string" + }, + "params": { + "additionalProperties": { + "anyOf": [ + { + "type": "string" + }, + { + "$ref": "#/$defs/OpenShellAnyMatcher" + } + ] + }, + "title": "Params", + "type": "object" + }, + "path": { + "default": "", + "title": "Path", + "type": "string" + }, + "query": { + "additionalProperties": { + "anyOf": [ + { + "type": "string" + }, + { + "$ref": "#/$defs/OpenShellAnyMatcher" + } + ] + }, + "title": "Query", + "type": "object" + }, + "tool": { + "anyOf": [ + { + "type": "string" + }, + { + "$ref": "#/$defs/OpenShellAnyMatcher" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Tool" + } + }, + "title": "OpenShellRequestMatcher", + "type": "object" + } + }, + "$id": "https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/host-grants-inventory-schema.v0.8.json", + "$ref": "#/$defs/HostGrantsInventoryV8", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "JSON Schema for shipgate audit --host --json. The inventory summarizes local coding-agent host grants and does not gate releases.", + "title": "Agents Shipgate Host Grants Inventory v0.8" +} diff --git a/docs/openshell-support.md b/docs/openshell-support.md new file mode 100644 index 00000000..f2117e94 --- /dev/null +++ b/docs/openshell-support.md @@ -0,0 +1,82 @@ +# OpenShell static policy inventory + +Agents Shipgate reads explicitly selected, repository-local OpenShell policy +documents with `shipgate audit --host --workspace . --json`. The inventory +describes document facts. It does not execute OpenShell, query a gateway, +resolve DNS, inspect a running sandbox, or assert tool effects or approvals. + +Register policies in `.shipgate/openshell.json`: + +```json +{ + "version": 1, + "runtime_version": "0.1.2", + "policies": [ + {"path": "configs/worker-policy.yaml", "role": "authored"} + ] +} +``` + +This registration is an Agents Shipgate format. OpenShell does not discover +policies from this filename. Paths are normalized, relative to the audited +workspace root, even when a registration is nested. Arbitrary filenames are +accepted. Each registration selects 1–64 distinct paths, with at most 64 policy +references across the workspace, including repeated selections. Globs, URLs, absolute +paths and `..` are rejected. A policy is read only when a registration selects +it. The detection census recognizes the registration filename without reading +the policy. It never classifies every YAML file as an OpenShell policy. + +`authored` identifies a proposed sandbox policy. `effective_snapshot` identifies +a locally supplied export in the same policy YAML/JSON shape. Both are static +documents reviewed independently. An export's presence establishes no live +freshness or enforcement. Provider/global composition is outside this reader's +scope. The runtime pin `0.1.2`, OpenShell policy schema `1`, registration schema +`1` and host inventory schema `0.8` are separate version axes. + +The `openshell_policy` grant contains typed filesystem, Landlock, process and +network facts. Endpoint and binary lists remain together under their rule; +they are not flattened into independent grants. `field_paths` and +`defaulted_fields` are JSON pointers. For example, an omitted endpoint +`enforcement` is recorded as `audit`; an explicitly supplied filesystem section +defaults `include_workdir` to `false`. When the entire filesystem section is +omitted, the recorded omission means workdir inclusion defaults to `true`. +Driver-selected process identity and runtime-added filesystem baseline paths +are named unresolved runtime context. Access/risk remain `unknown`: an HTTP +method or MCP tool name does not declare a business action's authority. + +Defaults follow the pinned [OpenShell v0.1.2 authored schema](https://github.com/NVIDIA/OpenShell/blob/v0.1.2/crates/openshell-policy-schema/src/lib.rs) +and [conversion code](https://github.com/NVIDIA/OpenShell/blob/v0.1.2/crates/openshell-policy/src/lib.rs). +The [upstream schema reference](https://docs.nvidia.com/openshell/how-it-works/policies/schema) +describes runtime constraints beyond document inventory. This reader is not a +substitute for upstream policy validation. Semantic expansion/subset review, +Git dependency identity, gate integration, local composition and native proof +are separate implementation stages (#944–#948). + +## Read limits and coverage + +The shared identity-bound host reader limits individual files to 1 MiB, along +with its aggregate byte/entry limits. OpenShell's own file limit is larger. +This adapter additionally limits YAML to 100,000 parser events and nesting depth +48, rejecting anchors, aliases, merge keys, duplicate/non-string mapping keys, +explicit YAML tags, multiple documents, explicit nulls and control characters. +Booleans use YAML 1.2 spelling; date-like MCP revisions remain strings. YAML and +JSON policy files use the same validation. Registration files must be JSON. + +Unknown fields/types, unsupported versions, malformed documents, missing or +unreadable paths, and unsupported middleware produce named blocking coverage +issues. They cannot become an empty complete inventory. `network_middlewares` +is recognized but not interpreted; its free-form configuration is never +published. The reader supports bounded in-tree file symlinks through the +shared read session; escaping, unresolved and directory links are coverage +limits. It never reads outside the workspace. + +Reports publish redacted evidence digests and sanitized errors, never parser +excerpts or credential values. If a credential-shaped authority label would +change under redaction, that document becomes unsupported instead of letting +different labels compare as equal. Exact file identity remains internal to the +read session. Historical v0.1–v0.7 host schemas remain frozen and readable; +current inventories/baselines/drift use v0.8. + +The checked-in [example](../samples/openshell/sandbox.yaml) is selected by +`samples/openshell/.shipgate/openshell.json` when auditing this repository root. +For another workspace, copy the policy and register its new local path. diff --git a/docs/triggers.json b/docs/triggers.json index 070db9bc..58f72ad3 100644 --- a/docs/triggers.json +++ b/docs/triggers.json @@ -21,6 +21,13 @@ "user_did_not_request": "The user did not explicitly ask for a Shipgate run in their prompt. Used in `stop_conditions` only." }, "boundary_adapters": [ + { + "id": "openshell", + "hosts": ["openshell"], + "exact_paths": [".shipgate/openshell.json"], + "globs": ["**/.shipgate/openshell.json"], + "experimental": false + }, { "id": "codex", "hosts": ["codex"], diff --git a/llms-full.txt b/llms-full.txt index e0fa5934..e4cb34bf 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -2399,7 +2399,7 @@ Downstream repos generated with - Current attestation schema: `0.5` — [`docs/attestation-schema.v0.5.json`](attestation-schema.v0.5.json) - Current registry schema: `0.4` — [`docs/registry-schema.v0.4.json`](registry-schema.v0.4.json) - Current org evidence bundle schema: `shipgate.org_evidence_bundle/v2` — [`docs/org-evidence-bundle-schema.v2.json`](org-evidence-bundle-schema.v2.json) -- Current host-grants inventory, baseline, and drift schemas: `0.7` — [`inventory`](host-grants-inventory-schema.v0.7.json), [`baseline`](host-grants-baseline-schema.v0.7.json), [`drift`](host-grants-drift-schema.v0.7.json) +- Current host-grants inventory, baseline, and drift schemas: `0.8` — [`inventory`](host-grants-inventory-schema.v0.8.json), [`baseline`](host-grants-baseline-schema.v0.8.json), [`drift`](host-grants-drift-schema.v0.8.json). Version 0.8 adds selected OpenShell document facts; historical host schemas remain frozen. See [OpenShell support](openshell-support.md). - Current trigger catalog schema: `0.4` — [`docs/triggers.json`](triggers.json) - Current governance benchmark catalog schema: `0.2` — [`docs/governance-benchmark-catalog-schema.v0.2.json`](governance-benchmark-catalog-schema.v0.2.json) - Current governance benchmark result schema: `0.2` — [`docs/governance-benchmark-result-schema.v0.2.json`](governance-benchmark-result-schema.v0.2.json) diff --git a/samples/openshell/.shipgate/openshell.json b/samples/openshell/.shipgate/openshell.json new file mode 100644 index 00000000..e26731f8 --- /dev/null +++ b/samples/openshell/.shipgate/openshell.json @@ -0,0 +1,7 @@ +{ + "version": 1, + "runtime_version": "0.1.2", + "policies": [ + {"path": "samples/openshell/sandbox.yaml", "role": "authored"} + ] +} diff --git a/samples/openshell/sandbox.yaml b/samples/openshell/sandbox.yaml new file mode 100644 index 00000000..ac8aa047 --- /dev/null +++ b/samples/openshell/sandbox.yaml @@ -0,0 +1,17 @@ +version: 1 +filesystem_policy: + include_workdir: false + read_only: [/usr, /lib, /etc] + read_write: [/tmp] +landlock: + compatibility: hard_requirement +network_policies: + github_read: + binaries: + - path: /usr/bin/gh + endpoints: + - host: api.github.com + port: 443 + protocol: rest + enforcement: enforce + access: read-only diff --git a/scripts/generate_schemas.py b/scripts/generate_schemas.py index 8a04e701..89c9f696 100644 --- a/scripts/generate_schemas.py +++ b/scripts/generate_schemas.py @@ -51,13 +51,13 @@ - docs/registry-schema.v0.4.json (from agents_shipgate.schemas.registry. RegistryQueryResultV1) -- docs/host-grants-inventory-schema.v0.7.json +- docs/host-grants-inventory-schema.v0.8.json (from agents_shipgate.schemas.host_grants. - HostGrantsInventoryArtifactV7) -- docs/host-grants-baseline-schema.v0.7.json - (from HostGrantsBaselineArtifactV7) -- docs/host-grants-drift-schema.v0.7.json - (from HostGrantsDriftArtifactV7) + HostGrantsInventoryArtifactV8) +- docs/host-grants-baseline-schema.v0.8.json + (from HostGrantsBaselineArtifactV8) +- docs/host-grants-drift-schema.v0.8.json + (from HostGrantsDriftArtifactV8) - docs/capability-lock-schema.v0.8.json (from agents_shipgate.schemas.capabilities. CapabilityLockFileArtifactV1) @@ -2494,10 +2494,10 @@ def build_host_grants_inventory_schema() -> tuple[Path, str]: from agents_shipgate.schemas.host_grants import ( HOST_GRANTS_INVENTORY_SCHEMA_VERSION, - HostGrantsInventoryArtifactV7, + HostGrantsInventoryArtifactV8, ) - schema = HostGrantsInventoryArtifactV7.model_json_schema() + schema = HostGrantsInventoryArtifactV8.model_json_schema() minor = HOST_GRANTS_INVENTORY_SCHEMA_VERSION schema["$id"] = ( "https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/" @@ -2518,10 +2518,10 @@ def build_host_grants_baseline_schema() -> tuple[Path, str]: from agents_shipgate.schemas.host_grants import ( HOST_GRANTS_BASELINE_SCHEMA_VERSION, - HostGrantsBaselineArtifactV7, + HostGrantsBaselineArtifactV8, ) - schema = HostGrantsBaselineArtifactV7.model_json_schema() + schema = HostGrantsBaselineArtifactV8.model_json_schema() minor = HOST_GRANTS_BASELINE_SCHEMA_VERSION schema["$id"] = ( "https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/" @@ -2541,10 +2541,10 @@ def build_host_grants_drift_schema() -> tuple[Path, str]: from agents_shipgate.schemas.host_grants import ( HOST_GRANTS_DRIFT_SCHEMA_VERSION, - HostGrantsDriftArtifactV7, + HostGrantsDriftArtifactV8, ) - schema = HostGrantsDriftArtifactV7.model_json_schema() + schema = HostGrantsDriftArtifactV8.model_json_schema() minor = HOST_GRANTS_DRIFT_SCHEMA_VERSION schema["$id"] = ( "https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/" diff --git a/src/agents_shipgate/core/boundary_registry.py b/src/agents_shipgate/core/boundary_registry.py index 2616266b..00acade8 100644 --- a/src/agents_shipgate/core/boundary_registry.py +++ b/src/agents_shipgate/core/boundary_registry.py @@ -29,6 +29,12 @@ def matches(self, path: str) -> bool: BOUNDARY_ADAPTERS: tuple[BoundaryAdapterSpec, ...] = ( + BoundaryAdapterSpec( + id="openshell", + hosts=("openshell",), + exact_paths=(".shipgate/openshell.json",), + globs=("**/.shipgate/openshell.json",), + ), BoundaryAdapterSpec( id="codex", hosts=("codex",), diff --git a/src/agents_shipgate/core/host_grants.py b/src/agents_shipgate/core/host_grants.py index d534808b..e48d77e3 100644 --- a/src/agents_shipgate/core/host_grants.py +++ b/src/agents_shipgate/core/host_grants.py @@ -25,7 +25,7 @@ from urllib.parse import parse_qsl, urlsplit, urlunsplit import yaml -from pydantic import ValidationError +from pydantic import BaseModel, ValidationError from agents_shipgate.core.boundary_registry import ( BOUNDARY_ADAPTERS, @@ -71,6 +71,13 @@ ) from agents_shipgate.core.jsonc import is_vscode_mcp_path, loads_jsonc from agents_shipgate.core.mcp_launch_source import launch_source_pin +from agents_shipgate.core.openshell import ( + OpenShellCollectionBudget, + OpenShellReadError, + parse_policy, + parse_selection, + policy_field_paths, +) from agents_shipgate.core.permission_lattice import ( exec_equivalent_argument, permission_pairing_group, @@ -95,8 +102,9 @@ HostGrantsBaselineV5, HostGrantsBaselineV6, HostGrantsBaselineV7, - HostGrantsDriftV7, - HostGrantsInventoryV7, + HostGrantsBaselineV8, + HostGrantsDriftV8, + HostGrantsInventoryV8, ) HOST_GRANTS_SCHEMA_VERSION = HOST_GRANTS_BASELINE_SCHEMA_VERSION @@ -165,6 +173,9 @@ class HostStaticParseCache: _parses: dict[ tuple[str, str], tuple[Any, str | None, str | None] ] = field(default_factory=dict) + _openshell_parses: dict[tuple[str, str, str], tuple[Any, str | None, str | None]] = field( + default_factory=dict, repr=False, + ) read_counts: dict[str, int] = field(default_factory=dict) parse_counts: dict[str, int] = field(default_factory=dict) _budget: IdentityReadBudget = field(init=False, repr=False) @@ -3800,6 +3811,132 @@ def unresolved_structure_message(reason: str) -> str: ) +def _collect_openshell( + *, path: Path, source: str, root: Path, cache: HostStaticParseCache, + artifacts: list[dict[str, Any]], grants: list[dict[str, Any]], + issues: list[dict[str, Any]], budget: OpenShellCollectionBudget, + resolved_through: tuple[str, ...] = (), +) -> None: + """Read registration and selected policies through the inventory's session.""" + + def read_document( + read_path: Path, label: str, kind: str, parser: Callable[[str], Any], + hops: tuple[str, ...] = (), + ) -> Any: + def record(artifact: dict[str, Any]) -> None: + if artifact["artifact_id"] not in budget.artifact_ids: + budget.artifact_ids.add(artifact["artifact_id"]) + artifacts.append(artifact) + + text, error = cache.read(read_path, containment_root=root) + if error: + record(_artifact( + host="openshell", scope="repository", source=label, kind=kind, + status="failed", resolved_through=hops, + )) + issues.append(cache.read_issue( + path=read_path, containment_root=root, source=label, host="openshell", + kind="unreadable", message=error, + )) + return None + assert text is not None + # Publish only a digest of structured public facts. Failed parsing + # leaves no digest: a hash of malformed, unredactable credential text + # could disclose a low-entropy value. Exact input identity is private. + evidence = None + try: + key = (*cache._key(read_path, root), kind) + if key not in cache._openshell_parses: + cache.parse_counts[str(read_path.absolute())] = ( + cache.parse_counts.get(str(read_path.absolute()), 0) + 1 + ) + try: + cache._openshell_parses[key] = (parser(text), None, None) + except OpenShellReadError as exc: + cache._openshell_parses[key] = (None, exc.kind, exc.message) + model, parse_kind, parse_message = cache._openshell_parses[key] + if parse_kind: + raise OpenShellReadError(parse_kind, parse_message or "OpenShell document was not read") + if isinstance(model, BaseModel): + raw = model.model_dump(mode="json") + public = _openshell_public_value(raw) + evidence = {"public_policy": public} + if public != raw: + raise OpenShellReadError( + "unsupported", "credential-shaped policy labels cannot be compared after redaction" + ) + except OpenShellReadError as exc: + record(_artifact( + host="openshell", scope="repository", source=label, kind=kind, + status="failed" if exc.kind == "parse_failed" else "unsupported", + data=evidence, resolved_through=hops, + )) + issues.append(_inventory_issue( + kind=exc.kind, host="openshell", source=label, message=exc.message, blocking=True, + )) + return None + record(_artifact( + host="openshell", scope="repository", source=label, kind=kind, + status="parsed", data=evidence, resolved_through=hops, + )) + return model + + selection = read_document(path, source, "openshell_selection", parse_selection, resolved_through) + if selection is None: + return + for reference in sorted(selection.policies, key=lambda item: item.path): + if not budget.reserve(): + issues.append(_inventory_issue( + kind="unsupported", host="openshell", source=source, + message="OpenShell selection exceeds the workspace limit of 64 policy references", + blocking=True, + )) + break + policy_path = root / reference.path + hops: tuple[str, ...] = () + reader = cache.reader_for(root) + try: + kind = reader.directory_entry_kind(Path(reference.path)) + except (OSError, ValueError): + kind = None + if kind == "symlink": + resolution = _resolve_in_tree_link(reader, Path(reference.path)) + if resolution is not None and resolution[1] == "file": + policy_path, hops = root / resolution[0], resolution[2] + policy = read_document(policy_path, reference.path, "openshell_policy", parse_policy, hops) + if policy is None: + continue + fields, defaults = policy_field_paths(policy) + facts = { + "registration": public_host_path(source), "role": reference.role, + "runtime_version": selection.runtime_version, "policy_schema_version": policy.version, + "policy": policy.model_dump(mode="json"), + "defaulted_fields": defaults, "field_paths": fields, + } + grants.append({ + **_grant_base( + host="openshell", scope="repository", source=reference.path, + kind="openshell_policy", identity=source, config=facts, + access="unknown", risk="unknown", + ), + "facts": facts, + }) + + +def _openshell_public_value(value: Any, *, parent_key: str = "") -> Any: + if isinstance(value, str): + return _sanitize_sensitive_string(redact_text(value) or "") + if isinstance(value, dict): + return {_openshell_public_value(key): ( + "" if parent_key in {"query", "params"} and _is_secret_key(key) + else _openshell_public_value(child, parent_key=key) + ) + for key, child in value.items()} + if isinstance(value, list): + return [_openshell_public_value(child) for child in value] + return value + + def _collect_file( *, path: Path, source: str, host: str, scope: HostScope, kind: str, containment_root: Path, cache: HostStaticParseCache, @@ -4916,7 +5053,10 @@ def _coverage( *, scope: HostScope, artifacts: list[dict[str, Any]], issues: list[dict[str, Any]] ) -> list[dict[str, Any]]: coverage: list[dict[str, Any]] = [] - for host in ("codex", "claude-code", "cursor", "vscode", "github"): + hosts = ["codex", "claude-code", "cursor", "vscode", "github"] + if any(item["host"] == "openshell" for item in (*artifacts, *issues)): + hosts.append("openshell") + for host in hosts: host_artifacts = [item for item in artifacts if item["host"] == host] host_issues = [item for item in issues if item["host"] == host and item["blocking"]] status = "partial" if host_issues else "complete" @@ -5228,7 +5368,16 @@ def note_unusable_selected_hooks(data: Any, *, source: str) -> None: bound_by_selecting_roots(item["issue_id"], source) collected_claude_sources: set[str] = set() + openshell_budget = OpenShellCollectionBudget() for path, source, host, kind, resolved_through in repository_paths: + if host == "openshell": + _collect_openshell( + path=path, source=source, root=root, cache=cache, + artifacts=artifacts, grants=grants, issues=issues, + budget=openshell_budget, + resolved_through=resolved_through, + ) + continue hook_basis: HookLoadingBasis = "host_configuration" if host == "claude-code" and kind == "hooks": # Claude Code documents no project `.claude/hooks/hooks.json` @@ -5292,6 +5441,13 @@ def note_unusable_selected_hooks(data: Any, *, source: str) -> None: else: # pragma: no cover - CLI and typing constrain this; defensive API guard. raise ValueError(f"Unsupported host audit scope: {scope!r}") + if any(item["host"] == "openshell" for item in artifacts): + excluded.extend([ + "OpenShell provider and gateway/global policy composition", + "OpenShell driver defaults, runtime-added filesystem baseline paths and live policy freshness", + "OpenShell runtime validation, DNS resolution, executable identity and native policy proof", + ]) + if scope == "local_static": grants, claude_precedence_issues = _project_claude_precedence(grants) issues.extend(claude_precedence_issues) @@ -5325,7 +5481,7 @@ def note_unusable_selected_hooks(data: Any, *, source: str) -> None: except ValueError: source = failure.source failure = replace(failure, source=source) - for host in ("codex", "claude-code", "cursor", "vscode", "github"): + for host in ("codex", "claude-code", "cursor", "vscode", "github", "openshell"): issue = _inventory_issue( kind="unreadable", host=host, source=failure.source, message=failure.summary() + " " + failure.recovery(), blocking=True, @@ -5351,7 +5507,7 @@ def note_unusable_selected_hooks(data: Any, *, source: str) -> None: "static_analysis_only": True, "runtime_session_verified": False, } - inventory = HostGrantsInventoryV7.model_validate(payload).model_dump(mode="json") + inventory = HostGrantsInventoryV8.model_validate(payload).model_dump(mode="json") return HostBoundarySnapshot( inventory=inventory, cache=cache, input_failures=dict(cache.input_failures), plugin_reference_issue_ids=frozenset(plugin_reference_issue_ids), @@ -5390,7 +5546,7 @@ def host_audit_inventory( if snapshot is None: snapshot = build_host_boundary_snapshot(workspace, scope=scope, cache=cache) - inventory = HostGrantsInventoryV7.model_validate(snapshot.inventory) + inventory = HostGrantsInventoryV8.model_validate(snapshot.inventory) if inventory.scope != scope: raise ValueError( f"Host boundary snapshot scope {inventory.scope!r} does not match {scope!r}" @@ -5623,7 +5779,7 @@ def build_host_grants_baseline(inventory: dict[str, Any]) -> dict[str, Any]: "grants": [compared_grant(grant) for grant in normalized["grants"]], }, } - return HostGrantsBaselineV7.model_validate(payload).model_dump(mode="json") + return HostGrantsBaselineV8.model_validate(payload).model_dump(mode="json") def host_comparison_baseline(inventory: dict[str, Any]) -> dict[str, Any]: @@ -5682,7 +5838,7 @@ def load_host_grants_baseline_with_text( "and repair or replace it deliberately." ) return data, text - if version not in {"0.2", "0.3", "0.4", "0.5", "0.6", HOST_GRANTS_BASELINE_SCHEMA_VERSION}: + if version not in {"0.2", "0.3", "0.4", "0.5", "0.6", "0.7", HOST_GRANTS_BASELINE_SCHEMA_VERSION}: raise ValueError( f"Host-grants baseline {path} has unsupported schema version " f"{version!r}. A human must review migration or replacement." @@ -5690,7 +5846,8 @@ def load_host_grants_baseline_with_text( try: model = {"0.2": HostGrantsBaselineV2, "0.3": HostGrantsBaselineV3, "0.4": HostGrantsBaselineV4, "0.5": HostGrantsBaselineV5, - "0.6": HostGrantsBaselineV6, "0.7": HostGrantsBaselineV7}[version] + "0.6": HostGrantsBaselineV6, "0.7": HostGrantsBaselineV7, + "0.8": HostGrantsBaselineV8}[version] parsed = model.model_validate(data).model_dump(mode="json") except ValidationError: return ( @@ -6506,7 +6663,7 @@ def _incomparable_payload( # and also route to a human before any first acknowledgement. "next_action": None, } - return HostGrantsDriftV7.model_validate(payload).model_dump(mode="json") + return HostGrantsDriftV8.model_validate(payload).model_dump(mode="json") #: Baseline versions a drift comparison reads as current. v0.5 only adds @@ -6518,11 +6675,11 @@ def _incomparable_payload( #: checkout refs (#823); the rules below narrow which older baselines that #: acceptance still covers. _COMPARABLE_BASELINE_SCHEMA_VERSIONS = frozenset( - {"0.4", "0.5", "0.6", HOST_GRANTS_BASELINE_SCHEMA_VERSION} + {"0.4", "0.5", "0.6", "0.7", HOST_GRANTS_BASELINE_SCHEMA_VERSION} ) #: Baselines without workflow grants retain the v0.6 replacement route (#819). -OVERWRITABLE_BASELINE_SCHEMA_VERSIONS = frozenset({"0.6", HOST_GRANTS_BASELINE_SCHEMA_VERSION}) +OVERWRITABLE_BASELINE_SCHEMA_VERSIONS = frozenset({"0.6", "0.7", HOST_GRANTS_BASELINE_SCHEMA_VERSION}) #: Baseline versions whose workflow grants never read step action references #: (#771). Such a grant's missing ``step_actions`` is not evidence that no @@ -6636,7 +6793,7 @@ def _comparable_drift_payload( "incomparable_reasons": [], "next_action": None, } - return HostGrantsDriftV7.model_validate(payload).model_dump(mode="json") + return HostGrantsDriftV8.model_validate(payload).model_dump(mode="json") def build_host_comparison_payload( @@ -6690,6 +6847,16 @@ def render_host_audit_markdown( else: for kind, grants in sorted(by_kind.items()): lines.append(f"- `{kind}`: {len(grants)}") + for grant in by_kind.get("openshell_policy", []): + facts = grant["facts"] + lines.extend([ + "", "### OpenShell selected document", "", + "Policy facts include correlated binary/endpoint rules and default provenance; " + "they make no tool effect or deployed enforcement claim.", + "", "~~~~json", + json.dumps({"source": grant["source"], **facts}, indent=2, sort_keys=True), + "~~~~", + ]) wildcard_rules = [ grant for grant in by_kind.get("permission_rule", []) diff --git a/src/agents_shipgate/core/openshell.py b/src/agents_shipgate/core/openshell.py new file mode 100644 index 00000000..dfe835c7 --- /dev/null +++ b/src/agents_shipgate/core/openshell.py @@ -0,0 +1,169 @@ +"""Bounded, offline parsing of explicitly selected OpenShell documents.""" + +from __future__ import annotations + +import json +import re +from dataclasses import dataclass, field +from typing import Any + +import yaml +from pydantic import BaseModel, ValidationError +from yaml.events import AliasEvent, CollectionEndEvent, CollectionStartEvent, ScalarEvent + +from agents_shipgate.schemas.openshell import OpenShellPolicy, OpenShellSelection + +MAX_OPENSHELL_BYTES = 1024 * 1024 +MAX_OPENSHELL_EVENTS = 100000 +MAX_OPENSHELL_DEPTH = 48 +MAX_OPENSHELL_REFERENCES = 64 + + +@dataclass +class OpenShellCollectionBudget: + references: int = 0 + artifact_ids: set[str] = field(default_factory=set) + + def reserve(self) -> bool: + self.references += 1 + return self.references <= MAX_OPENSHELL_REFERENCES + + +@dataclass +class OpenShellReadError(ValueError): + kind: str + message: str + + +class _PolicyLoader(yaml.SafeLoader): + # YAML 1.2 booleans; YAML 1.1 dates and on/off coercions are inappropriate + # for string-valued MCP revisions, methods, host names and matchers. + yaml_implicit_resolvers = { + key: [item for item in resolvers if item[0] not in { + "tag:yaml.org,2002:bool", "tag:yaml.org,2002:timestamp", + }] + for key, resolvers in yaml.SafeLoader.yaml_implicit_resolvers.items() + } + + def construct_mapping(self, node: yaml.MappingNode, deep: bool = False) -> dict[str, Any]: + mapping: dict[str, Any] = {} + for key_node, value_node in node.value: + key = self.construct_object(key_node, deep=deep) + if not isinstance(key, str) or key == "<<" or key in mapping: + raise OpenShellReadError("parse_failed", "duplicate, merge or non-string mapping key") + mapping[key] = self.construct_object(value_node, deep=deep) + return mapping + + +_PolicyLoader.add_implicit_resolver( + "tag:yaml.org,2002:bool", re.compile(r"^(?:true|false|True|False|TRUE|FALSE)$"), list("tTfF") +) + + +def load_document(text: str) -> dict[str, Any]: + """Reject expansion and depth hazards before constructing any YAML nodes. + + JSON is a YAML subset, so duplicate handling and bounds are identical. + Anchors/aliases and explicit tags are a deliberate unsupported subset. + """ + if len(text.encode("utf-8")) > MAX_OPENSHELL_BYTES: + raise OpenShellReadError("unsupported", "OpenShell document exceeds the 1 MiB static read limit") + depth = 0 + try: + for count, event in enumerate(yaml.parse(text), 1): + if count > MAX_OPENSHELL_EVENTS: + raise OpenShellReadError("unsupported", "OpenShell document exceeds the event limit") + if isinstance(event, AliasEvent) or getattr(event, "anchor", None): + raise OpenShellReadError("unsupported", "YAML anchors and aliases are not read") + if getattr(event, "tag", None): + raise OpenShellReadError("unsupported", "explicit YAML tags are not read") + if isinstance(event, CollectionStartEvent): + depth += 1 + if depth > MAX_OPENSHELL_DEPTH: + raise OpenShellReadError("unsupported", "OpenShell document exceeds the depth limit") + elif isinstance(event, CollectionEndEvent): + depth -= 1 + elif isinstance(event, ScalarEvent) and any(ord(char) < 32 for char in event.value): + raise OpenShellReadError("unsupported", "control characters in policy values are not read") + data = yaml.load(text, Loader=_PolicyLoader) + except (yaml.YAMLError, RecursionError, UnicodeError) as exc: + raise OpenShellReadError("parse_failed", "OpenShell static parser rejected the document") from exc + if not isinstance(data, dict): + raise OpenShellReadError("parse_failed", "OpenShell document must be an object") + _reject_nulls(data) + return data + + +def _reject_nulls(value: Any) -> None: + if value is None: + raise OpenShellReadError("unsupported", "explicit null values are not read") + if isinstance(value, dict): + for key, child in value.items(): + _reject_nulls(key) + _reject_nulls(child) + elif isinstance(value, list): + for child in value: + _reject_nulls(child) + elif isinstance(value, str): + try: + value.encode("utf-8") + except UnicodeError as exc: + raise OpenShellReadError("unsupported", "invalid Unicode policy value") from exc + + +def parse_selection(text: str) -> OpenShellSelection: + # The registration is JSON, not an upstream OpenShell format. + data = load_document(text) + try: + json.loads(text) + except (ValueError, RecursionError) as exc: + raise OpenShellReadError("parse_failed", "OpenShell registration must be JSON") from exc + return _validate(OpenShellSelection, data) + + +def parse_policy(text: str) -> OpenShellPolicy: + data = load_document(text) + model = _validate(OpenShellPolicy, data) + if model.network_middlewares: + raise OpenShellReadError("unsupported", "network_middlewares are outside static policy coverage") + return model + + +def _validate(model: type[BaseModel], data: dict[str, Any]) -> Any: + if type(data.get("version")) is not int: + raise OpenShellReadError("unsupported", "document version must be an integer") + try: + return model.model_validate(data) + except ValidationError as exc: + # Never include input values, dictionary keys or parser excerpts. + raise OpenShellReadError( + "unsupported", "unsupported version, field, type or policy constraint; see OpenShell coverage documentation" + ) from exc + + +def policy_field_paths(model: BaseModel, prefix: str = "") -> tuple[list[str], list[str]]: + """JSON pointers distinguish an omitted default from an authored value.""" + fields: list[str] = [] + defaults: list[str] = [] + for name in type(model).model_fields: + path = f"{prefix}/{name}" + fields.append(path) + if name not in model.model_fields_set: + defaults.append(path) + value = getattr(model, name) + children: list[tuple[str, BaseModel]] = [] + if isinstance(value, BaseModel): + children.append((path, value)) + elif isinstance(value, dict): + children.extend( + (f"{path}/{key.replace('~', '~0').replace('/', '~1')}", child) + for key, child in value.items() if isinstance(child, BaseModel) + ) + elif isinstance(value, list): + children.extend((f"{path}/{index}", child) for index, child in enumerate(value) + if isinstance(child, BaseModel)) + for child_path, child in children: + child_fields, child_defaults = policy_field_paths(child, child_path) + fields.extend(child_fields) + defaults.extend(child_defaults) + return sorted(fields), sorted(defaults) diff --git a/src/agents_shipgate/schemas/host_grants.py b/src/agents_shipgate/schemas/host_grants.py index 0532fd63..f22e5067 100644 --- a/src/agents_shipgate/schemas/host_grants.py +++ b/src/agents_shipgate/schemas/host_grants.py @@ -5,10 +5,11 @@ from pydantic import BaseModel, ConfigDict, Field, RootModel from agents_shipgate.schemas.instruction_structure import InstructionStructureEvidence +from agents_shipgate.schemas.openshell import OpenShellPolicyFacts -HOST_GRANTS_INVENTORY_SCHEMA_VERSION = "0.7" -HOST_GRANTS_BASELINE_SCHEMA_VERSION = "0.7" -HOST_GRANTS_DRIFT_SCHEMA_VERSION = "0.7" +HOST_GRANTS_INVENTORY_SCHEMA_VERSION = "0.8" +HOST_GRANTS_BASELINE_SCHEMA_VERSION = "0.8" +HOST_GRANTS_DRIFT_SCHEMA_VERSION = "0.8" HostName = Literal["codex", "claude-code", "cursor", "vscode", "github"] HostGrantScope = Literal["repository", "local_static"] @@ -1066,4 +1067,82 @@ class HostGrantsDriftArtifactV7(RootModel[HostGrantsDriftV7]): root: HostGrantsDriftV7 +HostNameV8 = Literal["codex", "claude-code", "cursor", "vscode", "github", "openshell"] + + +class HostOpenShellPolicyGrantV8(HostGrantBaseV2): + host: Literal["openshell"] = "openshell" + kind: Literal["openshell_policy"] = "openshell_policy" + facts: OpenShellPolicyFacts + + +HostGrantV8 = Annotated[HostGrantV7 | HostOpenShellPolicyGrantV8, Field(discriminator="kind")] +HostBaselineGrantV8 = Annotated[ + HostBaselineGrantV7 | HostOpenShellPolicyGrantV8, Field(discriminator="kind") +] + + +class HostArtifactV8(HostArtifactV7): + host: HostNameV8 + kind: Literal[ + "config", "mcp", "hooks", "workflow", "instructions", "requirements", "hook_script", + "openshell_selection", "openshell_policy", + ] + + +class HostCoverageV8(HostCoverageV2): + host: HostNameV8 + + +class HostInventoryIssueV8(HostInventoryIssueV2): + host: HostNameV8 + + +class HostCoverageChangeV8(HostCoverageChangeV2): + host: HostNameV8 + + +class HostArtifactChangeV8(HostArtifactChangeV7): + baseline: HostArtifactV8 | None = None + current: HostArtifactV8 | None = None + + +class HostGrantsInventoryV8(HostGrantsInventoryV7): + host_grants_inventory_schema_version: Literal["0.8"] = "0.8" + grants: list[HostGrantV8] = Field(default_factory=list) + artifacts: list[HostArtifactV8] = Field(default_factory=list) + host_coverage: list[HostCoverageV8] = Field(default_factory=list) + issues: list[HostInventoryIssueV8] = Field(default_factory=list) + + +class HostGrantsNormalizedSnapshotV8(HostGrantsNormalizedSnapshotV7): + grants: list[HostBaselineGrantV8] = Field(default_factory=list) + artifacts: list[HostArtifactV8] = Field(default_factory=list) + host_coverage: list[HostCoverageV8] = Field(default_factory=list) + + +class HostGrantsBaselineV8(HostGrantsBaselineV7): + host_grants_schema_version: Literal["0.8"] = "0.8" + inventory: HostGrantsNormalizedSnapshotV8 + + +class HostGrantsDriftV8(HostGrantsDriftV7): + host_grants_schema_version: Literal["0.8"] = "0.8" + artifact_changes: list[HostArtifactChangeV8] = Field(default_factory=list) + coverage_changes: list[HostCoverageChangeV8] = Field(default_factory=list) + issues: list[HostInventoryIssueV8] = Field(default_factory=list) + + +class HostGrantsInventoryArtifactV8(RootModel[HostGrantsInventoryV8]): + root: HostGrantsInventoryV8 + + +class HostGrantsBaselineArtifactV8(RootModel[HostGrantsBaselineV8]): + root: HostGrantsBaselineV8 + + +class HostGrantsDriftArtifactV8(RootModel[HostGrantsDriftV8]): + root: HostGrantsDriftV8 + + __all__ = [name for name in globals() if name.startswith("Host") or name.startswith("HOST_")] diff --git a/src/agents_shipgate/schemas/openshell.py b/src/agents_shipgate/schemas/openshell.py new file mode 100644 index 00000000..23483ec7 --- /dev/null +++ b/src/agents_shipgate/schemas/openshell.py @@ -0,0 +1,218 @@ +"""Static OpenShell policy facts, pinned independently of Shipgate schemas. + +These are document facts, never deployed bindings or tool effect declarations. +The authored schema follows NVIDIA/OpenShell v0.1.2 policy schema 1. +""" + +from __future__ import annotations + +from pathlib import PurePosixPath +from typing import Any, Literal + +from pydantic import BaseModel, ConfigDict, Field, model_validator + + +class OpenShellObject(BaseModel): + model_config = ConfigDict(extra="forbid", strict=True) + + +class OpenShellPolicyReference(OpenShellObject): + path: str + role: Literal["authored", "effective_snapshot"] + + @model_validator(mode="after") + def local_path(self) -> OpenShellPolicyReference: + path = PurePosixPath(self.path) + if ( + not self.path or path.is_absolute() or ".." in path.parts + or "\\" in self.path or ":" in self.path + or path.as_posix() != self.path or self.path == "." + ): + raise ValueError("policy path must be a normalized repository-relative path") + return self + + +class OpenShellSelection(OpenShellObject): + version: Literal[1] + runtime_version: Literal["0.1.2"] + policies: list[OpenShellPolicyReference] = Field(min_length=1, max_length=64) + + @model_validator(mode="after") + def distinct_paths(self) -> OpenShellSelection: + paths = [item.path for item in self.policies] + if len(paths) != len(set(paths)): + raise ValueError("each policy must be selected once") + return self + + +class OpenShellFilesystem(OpenShellObject): + include_workdir: bool = False + read_only: list[str] = Field(default_factory=list) + read_write: list[str] = Field(default_factory=list) + + @model_validator(mode="after") + def bounded_paths(self) -> OpenShellFilesystem: + if len(self.read_only) + len(self.read_write) > 256: + raise ValueError("too many filesystem paths") + for path in (*self.read_only, *self.read_write): + if not path.startswith("/") or ".." in path.split("/") or len(path.encode()) > 4096: + raise ValueError("invalid filesystem path") + if "/" in self.read_write: + raise ValueError("root cannot be writable") + return self + + +class OpenShellLandlock(OpenShellObject): + compatibility: Literal["best_effort", "hard_requirement"] = "best_effort" + + +class OpenShellProcess(OpenShellObject): + run_as_user: str = "" + run_as_group: str = "" + + @model_validator(mode="after") + def non_root_identity(self) -> OpenShellProcess: + for value in (self.run_as_user, self.run_as_group): + if value not in {"", "sandbox"} and not ( + value.isascii() and value.isdecimal() and 1 <= int(value) <= 4294967294 + ): + raise ValueError("invalid process identity") + return self + + +class OpenShellAnyMatcher(OpenShellObject): + any: list[str] = Field(min_length=1) + + +OpenShellMatcher = str | OpenShellAnyMatcher + + +class OpenShellRequestMatcher(OpenShellObject): + method: str = "" + path: str = "" + command: str = "" + query: dict[str, OpenShellMatcher] = Field(default_factory=dict) + operation_type: str = "" + operation_name: str = "" + fields: list[str] = Field(default_factory=list) + tool: OpenShellMatcher | None = None + params: dict[str, OpenShellMatcher] = Field(default_factory=dict) + + +class OpenShellAllowRule(OpenShellObject): + allow: OpenShellRequestMatcher + + +class OpenShellCredentialBinding(OpenShellObject): + provider: str + + +class OpenShellMcpOptions(OpenShellObject): + versions: list[str] = Field(default_factory=lambda: ["2025-11-25"]) + max_body_bytes: int = Field(default=65536, ge=0, le=4294967295) + strict_tool_names: bool = True + allow_all_known_mcp_methods: bool = False + + @model_validator(mode="after") + def supported_versions(self) -> OpenShellMcpOptions: + if not self.versions or len(set(self.versions)) != len(self.versions) or not set( + self.versions + ) <= {"2025-03-26", "2025-06-18", "2025-11-25"}: + raise ValueError("unsupported MCP versions") + return self + + +class OpenShellJsonRpcOptions(OpenShellObject): + max_body_bytes: int = Field(default=65536, ge=0, le=4294967295) + + +class OpenShellGraphqlOperation(OpenShellObject): + operation_type: str = "" + operation_name: str = "" + fields: list[str] = Field(default_factory=list) + + +class OpenShellEndpoint(OpenShellObject): + host: str = "" + port: int = Field(default=0, ge=0, le=65535) + ports: list[int] = Field(default_factory=list) + path: str = "" + protocol: Literal["", "rest", "websocket", "graphql", "mcp", "json-rpc", "tcp"] = "" + tls: Literal["", "skip"] = "" + enforcement: Literal["audit", "enforce", ""] = "audit" + access: Literal["", "read-only", "read-write", "full"] = "" + rules: list[OpenShellAllowRule] = Field(default_factory=list) + deny_rules: list[OpenShellRequestMatcher] = Field(default_factory=list) + allowed_ips: list[str] = Field(default_factory=list) + allow_encoded_slash: bool = False + websocket_credential_rewrite: bool = False + request_body_credential_rewrite: bool = False + allow_uninspected_credentials: bool = False + persisted_queries: Literal["", "deny", "allow_registered"] = "deny" + graphql_persisted_queries: dict[str, OpenShellGraphqlOperation] = Field(default_factory=dict) + graphql_max_body_bytes: int = Field(default=65536, ge=0, le=4294967295) + credential_signing: str | None = None + signing_service: str | None = None + signing_region: str | None = None + credential_binding: OpenShellCredentialBinding | None = None + json_rpc: OpenShellJsonRpcOptions | None = None + mcp: OpenShellMcpOptions | None = None + + @model_validator(mode="after") + def destination_shape(self) -> OpenShellEndpoint: + if any(type(port) is not int or not 1 <= port <= 65535 for port in self.ports): + raise ValueError("invalid endpoint ports") + if (self.port and self.ports) or not (self.port or self.ports): + raise ValueError("select port or ports") + if not self.host and not self.allowed_ips: + raise ValueError("endpoint needs host or allowed_ips") + if self.access and self.rules: + raise ValueError("access and rules are mutually exclusive") + if self.tls == "skip" and self.protocol not in {"", "tcp"}: + raise ValueError("TLS skip cannot inspect requests") + if self.protocol in {"rest", "websocket", "graphql"} and not (self.access or self.rules): + raise ValueError("inspected endpoint needs request grants") + if self.protocol in {"mcp", "json-rpc"} and not self.rules and not ( + self.protocol == "mcp" and self.mcp and self.mcp.allow_all_known_mcp_methods + ): + raise ValueError("endpoint needs request rules") + return self + + +class OpenShellBinary(OpenShellObject): + path: str + + +class OpenShellNetworkRule(OpenShellObject): + name: str = "" + endpoints: list[OpenShellEndpoint] = Field(default_factory=list) + binaries: list[OpenShellBinary] = Field(default_factory=list) + + +class OpenShellPolicy(OpenShellObject): + version: Literal[1] + filesystem_policy: OpenShellFilesystem | None = None + landlock: OpenShellLandlock | None = None + process: OpenShellProcess | None = None + network_policies: dict[str, OpenShellNetworkRule] = Field(default_factory=dict) + # Middleware is retained only as an explicit unsupported coverage signal. + # Its free-form config may carry credentials and is never published. + network_middlewares: dict[str, Any] = Field(default_factory=dict) + + +class OpenShellPolicyFacts(BaseModel): + model_config = ConfigDict(extra="forbid") + + registration: str + role: Literal["authored", "effective_snapshot"] + runtime_version: Literal["0.1.2"] + policy_schema_version: Literal[1] = 1 + policy: OpenShellPolicy + defaulted_fields: list[str] = Field(default_factory=list) + field_paths: list[str] = Field(default_factory=list) + include_workdir_when_filesystem_omitted: Literal[True] = True + landlock_when_omitted: Literal["best_effort"] = "best_effort" + process_omission: Literal["driver_default"] = "driver_default" + filesystem_baseline: Literal["runtime_dependent_not_resolved"] = "runtime_dependent_not_resolved" + # An exported file is not evidence that this policy is running now. + runtime_freshness_verified: Literal[False] = False diff --git a/tests/test_agent_instructions_apply.py b/tests/test_agent_instructions_apply.py index 19670c5e..2bf03e5a 100644 --- a/tests/test_agent_instructions_apply.py +++ b/tests/test_agent_instructions_apply.py @@ -205,9 +205,9 @@ def test_local_contract_renderer_has_required_fields() -> None: assert payload["registry_schema_version"] == "0.4" assert payload["org_evidence_bundle_schema_version"] == ("shipgate.org_evidence_bundle/v2") assert payload["agent_boundary_result_schema_version"] == ("shipgate.agent_boundary_result/v3") - assert payload["host_grants_inventory_schema_version"] == "0.7" - assert payload["host_grants_baseline_schema_version"] == "0.7" - assert payload["host_grants_drift_schema_version"] == "0.7" + assert payload["host_grants_inventory_schema_version"] == "0.8" + assert payload["host_grants_baseline_schema_version"] == "0.8" + assert payload["host_grants_drift_schema_version"] == "0.8" assert payload["trigger_catalog_schema_version"] == "0.4" assert payload["gating_signal"] == "release_decision.decision" assert payload["default_paths"]["local_contract"] == ".shipgate/agent-contract.json" diff --git a/tests/test_agent_instructions_renderers.py b/tests/test_agent_instructions_renderers.py index a6c12f43..73c19578 100644 --- a/tests/test_agent_instructions_renderers.py +++ b/tests/test_agent_instructions_renderers.py @@ -220,9 +220,9 @@ def test_local_contract_renderer_exposes_agent_operational_fields() -> None: assert payload["attestation_schema_version"] == "0.5" assert payload["registry_schema_version"] == "0.4" assert payload["org_evidence_bundle_schema_version"] == ("shipgate.org_evidence_bundle/v2") - assert payload["host_grants_inventory_schema_version"] == "0.7" - assert payload["host_grants_baseline_schema_version"] == "0.7" - assert payload["host_grants_drift_schema_version"] == "0.7" + assert payload["host_grants_inventory_schema_version"] == "0.8" + assert payload["host_grants_baseline_schema_version"] == "0.8" + assert payload["host_grants_drift_schema_version"] == "0.8" assert payload["trigger_catalog_schema_version"] == "0.4" assert payload["agent_result_control_fields"] == [ "decision", diff --git a/tests/test_hook_mcp_detail_fields.py b/tests/test_hook_mcp_detail_fields.py index de18b417..0ae76096 100644 --- a/tests/test_hook_mcp_detail_fields.py +++ b/tests/test_hook_mcp_detail_fields.py @@ -234,7 +234,7 @@ def test_the_grants_publish_the_detail_the_rows_render(tmp_path: Path) -> None: assert baseline["inventory"]["grants"] == [compared_grant(grant) for grant in inventory["grants"]] drift = build_host_drift_payload(baseline=baseline, inventory=inventory, baseline_file="b.json") for name, payload in (("inventory", inventory), ("baseline", baseline), ("drift", drift)): - schema = json.loads((ROOT / f"docs/host-grants-{name}-schema.v0.7.json").read_text()) + schema = json.loads((ROOT / f"docs/host-grants-{name}-schema.v0.8.json").read_text()) Draft202012Validator(schema).validate(payload) @@ -1227,7 +1227,7 @@ def test_a_0_6_baseline_stays_comparable_and_may_be_re_saved(tmp_path: Path) -> saved = json.loads(_invoke(["audit", "--host", "--workspace", str(root), "--save-baseline", "--json"])) assert saved["status"] == "updated" resaved = json.loads(path.read_text()) - assert resaved["host_grants_schema_version"] == "0.7" + assert resaved["host_grants_schema_version"] == "0.8" # Re-saving records the current comparison facts, the unresolved # reference's limit among them; nothing else moved. assert resaved == build_host_grants_baseline(_inventory(root)) @@ -1318,7 +1318,7 @@ def test_a_local_static_baseline_holds_no_home_directory_detail( for fact in ("canary", "homematcher", "homepkg", kinds["hook"]["handlers"][0]["command"]["sha256"]): assert fact not in text baseline = json.loads(text) - assert baseline["host_grants_schema_version"] == "0.7" + assert baseline["host_grants_schema_version"] == "0.8" assert _saved_detail(baseline) == [] # It still acknowledges both grants, and the next drift compares as before. assert sorted(grant["kind"] for grant in baseline["inventory"]["grants"]) == ["hook", "mcp_server"] diff --git a/tests/test_host_audit.py b/tests/test_host_audit.py index e6e70e1f..5f3ecb5e 100644 --- a/tests/test_host_audit.py +++ b/tests/test_host_audit.py @@ -30,10 +30,10 @@ load_host_grants_baseline, ) from agents_shipgate.schemas.host_grants import ( - HostGrantsBaselineV7, - HostGrantsDriftV7, + HostGrantsBaselineV8, + HostGrantsDriftV8, HostGrantsInventoryArtifactV4, - HostGrantsInventoryV7, + HostGrantsInventoryV8, ) runner = CliRunner() @@ -170,8 +170,8 @@ def _drift_json(tmp_path: Path, *extra: str) -> tuple[int, dict]: def test_inventory_v02_collects_typed_multi_host_grants(tmp_path: Path) -> None: inventory = host_audit_inventory(_seed_workspace(tmp_path)) - assert inventory["host_grants_inventory_schema_version"] == "0.7" - HostGrantsInventoryV7.model_validate(inventory) + assert inventory["host_grants_inventory_schema_version"] == "0.8" + HostGrantsInventoryV8.model_validate(inventory) assert inventory["scope"] == "repository" assert inventory["static_analysis_only"] is True assert inventory["runtime_session_verified"] is False @@ -554,7 +554,7 @@ def test_inventory_coverage_paths_are_owned_by_central_boundary_registry(tmp_pat inventory = host_audit_inventory(tmp_path) registered = { path - for adapter in BOUNDARY_ADAPTERS + for adapter in BOUNDARY_ADAPTERS if adapter.id != "openshell" for path in (*adapter.exact_paths, *adapter.globs) } reported = { @@ -749,8 +749,8 @@ def test_v02_baseline_is_typed_portable_redacted_and_idempotent(tmp_path: Path) _seed_workspace(tmp_path) baseline_path = _save_baseline(tmp_path) payload = json.loads(baseline_path.read_text(encoding="utf-8")) - HostGrantsBaselineV7.model_validate(payload) - assert payload["host_grants_schema_version"] == "0.7" + HostGrantsBaselineV8.model_validate(payload) + assert payload["host_grants_schema_version"] == "0.8" assert payload["scope"] == "repository" assert "workspace" not in payload["inventory"] assert payload["inventory"]["artifacts"] @@ -956,7 +956,7 @@ def test_clean_and_changed_v02_drift(tmp_path: Path) -> None: _save_baseline(tmp_path) code, clean = _drift_json(tmp_path) assert code == 0 - HostGrantsDriftV7.model_validate(clean) + HostGrantsDriftV8.model_validate(clean) assert clean["comparison_status"] == "comparable" assert clean["has_drift"] is False assert clean["baseline_sha256"] == clean["current_sha256"] @@ -1213,14 +1213,14 @@ def test_legacy_v01_baseline_is_incomparable_advisory_and_strict_20(tmp_path: Pa inventory=host_audit_inventory(tmp_path), baseline_file=".agents-shipgate/host-grants.json", ) - HostGrantsDriftV7.model_validate(shared) + HostGrantsDriftV8.model_validate(shared) assert shared["comparison_status"] == "incomparable" assert shared["next_action"] is None assert "--save-baseline" not in json.dumps(shared) code, payload = _drift_json(tmp_path) assert code == 0 - HostGrantsDriftV7.model_validate(payload) + HostGrantsDriftV8.model_validate(payload) assert payload["comparison_status"] == "incomparable" assert payload["has_drift"] is None assert "baseline_schema_v0.1" in payload["incomparable_reasons"][0] @@ -1272,7 +1272,7 @@ def test_malformed_nested_v02_baseline_is_incomparable_not_a_crash(tmp_path: Pat ) code, payload = _drift_json(tmp_path) assert code == 0 - HostGrantsDriftV7.model_validate(payload) + HostGrantsDriftV8.model_validate(payload) assert payload["comparison_status"] == "incomparable" assert payload["has_drift"] is None assert payload["incomparable_reasons"] == ["malformed_v0.2_baseline"] @@ -1629,9 +1629,9 @@ def denied_read_text( def test_generated_models_reject_unknown_fields_and_invalid_literals(tmp_path: Path) -> None: payload = host_audit_inventory(tmp_path) with pytest.raises(ValidationError): - HostGrantsInventoryV7.model_validate({**payload, "legacy_parse_warnings": []}) + HostGrantsInventoryV8.model_validate({**payload, "legacy_parse_warnings": []}) with pytest.raises(ValidationError): - HostGrantsInventoryV7.model_validate({**payload, "scope": "runtime"}) + HostGrantsInventoryV8.model_validate({**payload, "scope": "runtime"}) def test_inventory_schema_uses_discriminated_typed_grants() -> None: diff --git a/tests/test_host_comparison_coverage.py b/tests/test_host_comparison_coverage.py index c9519f55..553576bd 100644 --- a/tests/test_host_comparison_coverage.py +++ b/tests/test_host_comparison_coverage.py @@ -1850,7 +1850,7 @@ def test_a_blocking_source_a_reviewer_can_repair_outranks_routine_limits(tmp_pat assert {item["limit"] for item in coverage["items"][1:]} == {"unsupported"} block = _block(text) assert block[1].startswith( - " notes.md (claude-code, codex, cursor): unreadable in base and head" + " notes.md (claude-code, codex, cursor, openshell): unreadable in base and head" ) # Truncation is in the text, not only in the JSON integer beside it. assert block[-1] == " 3 more items not listed, each ranked below those above" diff --git a/tests/test_host_discovery.py b/tests/test_host_discovery.py index d0ea8739..13e2923d 100644 --- a/tests/test_host_discovery.py +++ b/tests/test_host_discovery.py @@ -391,6 +391,8 @@ def test_a_link_to_a_directory_still_withholds_the_negative(tmp_path, zero): # nothing failing. Pinning the decision per registry entry makes that a failing # test and a deliberate choice instead (PR #614 review). REGISTRY_ELIGIBILITY: dict[str, bool] = { + ".shipgate/openshell.json": True, + "**/.shipgate/openshell.json": True, ".codex/config.toml": True, ".codex/hooks.json": True, # Same document, same decision, different host: a hook declaration is diff --git a/tests/test_host_input_recovery.py b/tests/test_host_input_recovery.py index 9a287f30..df153e8d 100644 --- a/tests/test_host_input_recovery.py +++ b/tests/test_host_input_recovery.py @@ -303,6 +303,6 @@ def fail(self): snapshot = build_host_boundary_snapshot(tmp_path) for name, payload in ( ("agent-boundary-result-schema.v3.json", _result(tmp_path, snapshot)), - ("host-grants-inventory-schema.v0.7.json", snapshot.inventory), + ("host-grants-inventory-schema.v0.8.json", snapshot.inventory), ): jsonschema.validate(payload, json.loads((Path("docs") / name).read_text())) diff --git a/tests/test_instruction_structure_contracts.py b/tests/test_instruction_structure_contracts.py index d08f506f..3c3fee1b 100644 --- a/tests/test_instruction_structure_contracts.py +++ b/tests/test_instruction_structure_contracts.py @@ -49,7 +49,7 @@ def test_old_models_reject_structural_claims_and_old_baseline_is_not_restamped(r assert drift["has_drift"] is None assert "baseline_instruction_structure_unavailable" in drift["incomparable_reasons"] assert path.read_bytes() == captured - schema = json.loads((ROOT / "docs/host-grants-inventory-schema.v0.7.json").read_text()) + schema = json.loads((ROOT / "docs/host-grants-inventory-schema.v0.8.json").read_text()) Draft202012Validator(schema).validate(inventory) diff --git a/tests/test_local_contract.py b/tests/test_local_contract.py index 917cdc18..b0eb876c 100644 --- a/tests/test_local_contract.py +++ b/tests/test_local_contract.py @@ -156,9 +156,9 @@ def test_local_agent_contract_is_minimal_agent_operational_payload() -> None: assert payload["attestation_schema_version"] == "0.5" assert payload["registry_schema_version"] == "0.4" assert payload["org_evidence_bundle_schema_version"] == ("shipgate.org_evidence_bundle/v2") - assert payload["host_grants_inventory_schema_version"] == "0.7" - assert payload["host_grants_baseline_schema_version"] == "0.7" - assert payload["host_grants_drift_schema_version"] == "0.7" + assert payload["host_grants_inventory_schema_version"] == "0.8" + assert payload["host_grants_baseline_schema_version"] == "0.8" + assert payload["host_grants_drift_schema_version"] == "0.8" assert payload["trigger_catalog_schema_version"] == "0.4" assert payload["agent_result_schema_version"] == "agent_result_v3" assert payload["agent_result_schema_path"] == "docs/agent-result-schema.v3.json" diff --git a/tests/test_openshell_inventory.py b/tests/test_openshell_inventory.py new file mode 100644 index 00000000..faf8d82a --- /dev/null +++ b/tests/test_openshell_inventory.py @@ -0,0 +1,218 @@ +from __future__ import annotations + +import json +from pathlib import Path + +import pytest +from jsonschema import Draft202012Validator +from pydantic import ValidationError + +from agents_shipgate.core.host_grants import ( + HostStaticParseCache, + build_host_boundary_snapshot, + build_host_drift_payload, + build_host_grants_baseline, + host_audit_inventory, + inventory_is_complete, + render_host_audit_markdown, +) +from agents_shipgate.core.openshell import OpenShellReadError, parse_policy, parse_selection +from agents_shipgate.schemas.host_grants import ( + HostGrantsInventoryArtifactV7, + HostGrantsInventoryV7, +) + +POLICY = """version: 1 +filesystem_policy: + read_only: [/usr] + read_write: [/tmp] +network_policies: + github: + binaries: [{path: /usr/bin/gh}] + endpoints: + - host: api.github.com + port: 443 + protocol: rest + access: read-only +""" + + +def select(root: Path, *, role: str = "authored", path: str = "configs/arbitrary.rules") -> Path: + registration = root / ".shipgate/openshell.json" + registration.parent.mkdir(parents=True, exist_ok=True) + registration.write_text(json.dumps({ + "version": 1, "runtime_version": "0.1.2", "policies": [{"path": path, "role": role}], + })) + policy = root / path + policy.parent.mkdir(parents=True, exist_ok=True) + policy.write_text(POLICY) + return policy + + +def test_selected_arbitrary_filename_has_typed_facts_and_defaults(tmp_path: Path) -> None: + select(tmp_path) + inventory = host_audit_inventory(tmp_path) + assert inventory_is_complete(inventory) + grant, = inventory["grants"] + assert grant["host"] == "openshell" and grant["access"] == "unknown" + facts = grant["facts"] + assert facts["role"] == "authored" and facts["runtime_version"] == "0.1.2" + assert facts["policy_schema_version"] == 1 + endpoint = facts["policy"]["network_policies"]["github"]["endpoints"][0] + assert endpoint["enforcement"] == "audit" + assert facts["policy"]["filesystem_policy"]["include_workdir"] is False + assert "/filesystem_policy/include_workdir" in facts["defaulted_fields"] + assert "/network_policies/github/endpoints/0/enforcement" in facts["defaulted_fields"] + assert facts["runtime_freshness_verified"] is False + assert "openshell_policy" in render_host_audit_markdown(inventory) + schema = json.loads((Path(__file__).parents[1] / "docs/host-grants-inventory-schema.v0.8.json").read_text()) + Draft202012Validator(schema).validate(inventory) + with pytest.raises(ValidationError): + HostGrantsInventoryV7.model_validate(inventory) + + +def test_effective_snapshot_role_is_explicit_and_is_not_runtime_attestation(tmp_path: Path) -> None: + select(tmp_path, role="effective_snapshot") + facts = host_audit_inventory(tmp_path)["grants"][0]["facts"] + assert facts["role"] == "effective_snapshot" + assert not facts["runtime_freshness_verified"] + + +def test_v08_baseline_round_trip_and_frozen_v07_schema(tmp_path: Path) -> None: + select(tmp_path) + inventory = host_audit_inventory(tmp_path) + baseline = build_host_grants_baseline(inventory) + drift = build_host_drift_payload(baseline=baseline, inventory=inventory, baseline_file="baseline.json") + assert drift["comparison_status"] == "comparable" and drift["has_drift"] is False + old = json.loads((Path(__file__).parents[1] / "docs/host-grants-inventory-schema.v0.7.json").read_text()) + frozen = HostGrantsInventoryArtifactV7.model_json_schema() + for key in ("$id", "$schema", "title", "description"): + old.pop(key, None) + frozen.pop(key, None) + assert old == frozen + + +def test_unselected_yaml_is_not_a_policy(tmp_path: Path) -> None: + (tmp_path / "sandbox.yaml").write_text(POLICY) + assert host_audit_inventory(tmp_path)["grants"] == [] + + +@pytest.mark.parametrize("text", [ + "", "[]", "version: 1\nversion: 1", "version: true", "version: 2", + "version: 1\nunknown: true", "version: 1\nfilesystem_policy: null", + "version: 1\nfilesystem_policy: {include_workdir: on}", + "version: 1\nfilesystem_policy: {read_write: [/]}" , + "version: 1\nfilesystem_policy: {read_only: [/tmp/../etc]}", + "version: 1\nprocess: {run_as_user: '0'}", + "version: 1\nnetwork_policies: {rule: {endpoints: [{host: api.example.com, port: 70000}]}}", + "version: 1\nnetwork_middlewares: {redactor: {config: {token: secret}}}", + "version: 1\nx: &a [1]\ny: *a", "version: 1\nfilesystem_policy: {<<: {read_only: [/usr]}}", + "version: 1\n---\nversion: 1", "version: 1\nx: !!python/object:danger {}", + '{"version": 1, "process": {"run_as_user": "\\ud800"}}', +]) +def test_rejected_input_is_named_partial_not_empty_complete(tmp_path: Path, text: str) -> None: + select(tmp_path).write_text(text) + inventory = host_audit_inventory(tmp_path) + assert not inventory_is_complete(inventory) + assert inventory["grants"] == [] + issue, = inventory["issues"] + assert issue["blocking"] and issue["source"] == "configs/arbitrary.rules" + assert next(item for item in inventory["host_coverage"] if item["host"] == "openshell")["status"] == "partial" + + +@pytest.mark.parametrize("path", ["../escape.yaml", "/tmp/escape.yaml", "a/../../escape", "https://example.com/policy", "a\\b", "a/./b"]) +def test_registration_refuses_escape_remote_and_noncanonical_paths(path: str) -> None: + with pytest.raises(OpenShellReadError): + parse_selection(json.dumps({"version": 1, "runtime_version": "0.1.2", "policies": [{"path": path, "role": "authored"}]})) + + +def test_missing_policy_and_outside_symlink_do_not_read_external_bytes(tmp_path: Path) -> None: + policy = select(tmp_path) + policy.unlink() + assert not inventory_is_complete(host_audit_inventory(tmp_path)) + policy.symlink_to("/etc/passwd") + inventory = host_audit_inventory(tmp_path) + assert not inventory_is_complete(inventory) + assert inventory["grants"] == [] + + +def test_in_tree_link_and_repeated_selection_use_one_bound_read(tmp_path: Path) -> None: + policy = select(tmp_path) + target = tmp_path / "actual.yaml" + policy.rename(target) + policy.symlink_to("../actual.yaml") + nested = tmp_path / "project/.shipgate/openshell.json" + nested.parent.mkdir(parents=True) + nested.write_bytes((tmp_path / ".shipgate/openshell.json").read_bytes()) + cache = HostStaticParseCache() + inventory = build_host_boundary_snapshot(tmp_path, cache=cache).inventory + assert inventory_is_complete(inventory) + assert cache.read_counts[str(target)] == 1 + assert cache.parse_counts[str(target)] == 1 + assert len([item for item in inventory["artifacts"] if item["kind"] == "openshell_policy"]) == 1 + assert len(inventory["grants"]) == 2 + artifact = next(item for item in inventory["artifacts"] if item["kind"] == "openshell_policy") + assert artifact["resolved_through"] == ["actual.yaml"] + + +def test_bounds_and_duplicate_registration(tmp_path: Path) -> None: + with pytest.raises(OpenShellReadError): + parse_policy("version: 1\nx: " + "[" * 60 + "0" + "]" * 60) + policy = select(tmp_path) + policy.write_text("#" + "x" * (1024 * 1024)) + assert not inventory_is_complete(host_audit_inventory(tmp_path)) + with pytest.raises(OpenShellReadError): + parse_selection('{"version":1,"version":1,"runtime_version":"0.1.2","policies":[]}') + + +def test_repeated_registrations_cannot_multiply_projection_without_a_bound(tmp_path: Path) -> None: + references = [] + for index in range(33): + path = f"policy-{index}.yaml" + (tmp_path / path).write_text("version: 1") + references.append({"path": path, "role": "authored"}) + for prefix in ("a", "b"): + registration = tmp_path / prefix / ".shipgate/openshell.json" + registration.parent.mkdir(parents=True) + registration.write_text(json.dumps({"version": 1, "runtime_version": "0.1.2", "policies": references})) + inventory = host_audit_inventory(tmp_path) + assert not inventory_is_complete(inventory) + assert len(inventory["grants"]) == 64 + assert any("64 policy references" in issue["message"] for issue in inventory["issues"]) + + +def test_credentials_never_reach_json_markdown_or_errors(tmp_path: Path) -> None: + token = "ghp_" + "a" * 30 + policy = select(tmp_path) + policy.write_text(POLICY.replace("api.github.com", token)) + inventory = host_audit_inventory(tmp_path) + assert not inventory_is_complete(inventory) + assert token not in json.dumps(inventory) + render_host_audit_markdown(inventory) + policy.write_text("version: 1\nunknown: " + token) + assert token not in json.dumps(host_audit_inventory(tmp_path)) + policy.write_text(POLICY.replace("access: read-only", "rules: [{allow: {method: GET, path: /, query: {api_key: PRIVATE_VALUE}}}]")) + inventory = host_audit_inventory(tmp_path) + assert not inventory_is_complete(inventory) + assert "PRIVATE_VALUE" not in json.dumps(inventory) + render_host_audit_markdown(inventory) + + +def test_deterministic_inventory_and_distinct_document_versions(tmp_path: Path) -> None: + select(tmp_path) + assert host_audit_inventory(tmp_path) == host_audit_inventory(tmp_path) + registration = tmp_path / ".shipgate/openshell.json" + registration.write_text(registration.read_text().replace("0.1.2", "1.0")) + assert not inventory_is_complete(host_audit_inventory(tmp_path)) + + +def test_mcp_options_and_string_revisions_are_read_without_tool_effect_claims() -> None: + policy = parse_policy("""version: 1 +network_policies: + mcp: + binaries: [{path: /usr/bin/python}] + endpoints: + - host: mcp.example.com + port: 443 + protocol: mcp + mcp: {versions: [2025-03-26], allow_all_known_mcp_methods: true} +""") + assert policy.network_policies["mcp"].endpoints[0].mcp.versions == ["2025-03-26"] diff --git a/tests/test_org_governance.py b/tests/test_org_governance.py index c2eb5799..1aa251ba 100644 --- a/tests/test_org_governance.py +++ b/tests/test_org_governance.py @@ -575,7 +575,7 @@ def test_org_bundle_projects_platform_artifacts_without_second_gate( assert payload["registry_row"]["source_attestation_sha256"] == attestation_sha256 assert payload["org_status"]["summary"]["policy_pack_count"] == 1 assert payload["policy_packs"][0]["status"] == "verified" - assert payload["host_grants"]["host_grants_inventory_schema_version"] == "0.7" + assert payload["host_grants"]["host_grants_inventory_schema_version"] == "0.8" assert payload["artifacts"]["verifier"]["sha256"] diff --git a/tests/test_reusable_workflow_secret_mappings.py b/tests/test_reusable_workflow_secret_mappings.py index 5ecfeef0..3381cf72 100644 --- a/tests/test_reusable_workflow_secret_mappings.py +++ b/tests/test_reusable_workflow_secret_mappings.py @@ -807,9 +807,9 @@ def test_a_current_baseline_compares_mappings_and_validates_against_the_schemas( path.write_text(STAGING) inventory = host_audit_inventory(tmp_path) baseline = build_host_grants_baseline(inventory) - assert baseline["host_grants_schema_version"] == "0.7" - Draft202012Validator(json.loads((ROOT / "docs/host-grants-inventory-schema.v0.7.json").read_text())).validate(inventory) - Draft202012Validator(json.loads((ROOT / "docs/host-grants-baseline-schema.v0.7.json").read_text())).validate(baseline) + assert baseline["host_grants_schema_version"] == "0.8" + Draft202012Validator(json.loads((ROOT / "docs/host-grants-inventory-schema.v0.8.json").read_text())).validate(inventory) + Draft202012Validator(json.loads((ROOT / "docs/host-grants-baseline-schema.v0.8.json").read_text())).validate(baseline) unchanged = build_host_drift_payload(baseline=baseline, inventory=inventory, baseline_file="b.json") assert (unchanged["comparison_status"], unchanged["has_drift"]) == ("comparable", False) @@ -818,7 +818,7 @@ def test_a_current_baseline_compares_mappings_and_validates_against_the_schemas( drift = build_host_drift_payload(baseline=baseline, inventory=host_audit_inventory(tmp_path), baseline_file="b.json") assert drift["comparison_status"] == "comparable" and drift["has_drift"] is True assert len(drift["changes"]) == 1 and drift["expansion_signals"] == [] - Draft202012Validator(json.loads((ROOT / "docs/host-grants-drift-schema.v0.7.json").read_text())).validate(drift) + Draft202012Validator(json.loads((ROOT / "docs/host-grants-drift-schema.v0.8.json").read_text())).validate(drift) def test_a_saved_baseline_listing_mappings_out_of_order_still_compares_equal(tmp_path): diff --git a/tests/test_workflow_agent_launches.py b/tests/test_workflow_agent_launches.py index 700f4149..49af0a17 100644 --- a/tests/test_workflow_agent_launches.py +++ b/tests/test_workflow_agent_launches.py @@ -1911,7 +1911,7 @@ def test_a_v0_6_baseline_without_a_workflow_stays_comparable_and_can_be_replaced audit = ["audit", "--host", "--workspace", str(tmp_path), "--baseline-file", str(path)] resaved = CliRunner().invoke(app, [*audit, "--save-baseline"]) assert resaved.exit_code == 0, resaved.output - assert json.loads(path.read_text())["host_grants_schema_version"] == "0.7" + assert json.loads(path.read_text())["host_grants_schema_version"] == "0.8" def test_the_documented_migration_from_a_v0_6_baseline_holding_a_workflow(tmp_path): @@ -1965,18 +1965,18 @@ def test_a_current_baseline_compares_agent_launches_and_validates_against_the_sc path.write_text(_yaml(_reproduction(run="npm ci && claude -p 'x'", ref=HEAD_SHA))) inventory = host_audit_inventory(tmp_path) baseline = build_host_grants_baseline(inventory) - assert baseline["host_grants_schema_version"] == "0.7" + assert baseline["host_grants_schema_version"] == "0.8" workflow, = [grant for grant in baseline["inventory"]["grants"] if grant["kind"] == "workflow"] assert workflow["unread_agent_runs"] == [{"job": "review", "step": "steps[2]", "agent": "claude"}] for name, payload in (("inventory", inventory), ("baseline", baseline)): - schema = json.loads((ROOT / f"docs/host-grants-{name}-schema.v0.7.json").read_text()) + schema = json.loads((ROOT / f"docs/host-grants-{name}-schema.v0.8.json").read_text()) Draft202012Validator(schema).validate(payload) path.write_text(_yaml(_reproduction(claude_args="--dangerously-skip-permissions", ref=HEAD_SHA))) drift = build_host_drift_payload(baseline=baseline, inventory=host_audit_inventory(tmp_path), baseline_file="b.json") assert (drift["comparison_status"], drift["has_drift"]) == ("comparable", True) assert drift["expansion_signals"] == [f"workflow_agent_widened_changed: {SOURCE}"] - schema = json.loads((ROOT / "docs/host-grants-drift-schema.v0.7.json").read_text()) + schema = json.loads((ROOT / "docs/host-grants-drift-schema.v0.8.json").read_text()) Draft202012Validator(schema).validate(drift) diff --git a/tests/test_workflow_step_action_references.py b/tests/test_workflow_step_action_references.py index 3f09b386..9460ea34 100644 --- a/tests/test_workflow_step_action_references.py +++ b/tests/test_workflow_step_action_references.py @@ -460,7 +460,7 @@ def test_a_current_baseline_compares_step_references(tmp_path): path.parent.mkdir(parents=True) path.write_text(_yaml({"uses": f"actions/checkout@{PINNED}"})) baseline = build_host_grants_baseline(host_audit_inventory(tmp_path)) - assert baseline["host_grants_schema_version"] == "0.7" + assert baseline["host_grants_schema_version"] == "0.8" path.write_text(_yaml({"uses": "actions/checkout@main"})) drift = build_host_drift_payload(baseline=baseline, inventory=host_audit_inventory(tmp_path), baseline_file="b.json") @@ -830,7 +830,7 @@ def test_saving_over_a_legacy_baseline_without_a_workflow_is_refused(tmp_path, v path.rename(path.with_name(f"host-grants.v{version}.json")) resaved = CliRunner().invoke(app, [*audit, "--save-baseline"]) assert resaved.exit_code == 0, _output(resaved) - assert json.loads(path.read_text())["host_grants_schema_version"] == "0.7" + assert json.loads(path.read_text())["host_grants_schema_version"] == "0.8" def _output(result) -> str: @@ -959,7 +959,7 @@ def test_the_documented_migration_from_a_legacy_baseline_holding_a_workflow(tmp_ path.rename(path.with_name("host-grants.v0.5.json")) resaved = CliRunner().invoke(app, [*audit, "--save-baseline"]) assert resaved.exit_code == 0, resaved.output - assert json.loads(path.read_text())["host_grants_schema_version"] == "0.7" + assert json.loads(path.read_text())["host_grants_schema_version"] == "0.8" after = json.loads(CliRunner().invoke(app, [*audit, "--drift", "--json"]).stdout) assert (after["comparison_status"], after["has_drift"]) == ("comparable", False) assert path.with_name("host-grants.v0.5.json").read_text() == original diff --git a/tools/shipgate-detect.py b/tools/shipgate-detect.py index aa3ac877..a1fbe788 100644 --- a/tools/shipgate-detect.py +++ b/tools/shipgate-detect.py @@ -5984,6 +5984,7 @@ def _conventional_dir_locations( # compare every root/nested predicate with the canonical registry. These are # applicability names, not a second host parser or permission model. HOST_CONFIG_PATHS = { + ".shipgate/openshell.json": ["openshell"], ".codex/config.toml": ["codex"], ".codex/hooks.json": ["codex"], ".codex/requirements.toml": ["codex"], @@ -6004,6 +6005,7 @@ def _conventional_dir_locations( #: nesting path added later, and the conformance test only caught it #: because it probes a `nested/` variant of every registry glob (#689). HOST_CONFIG_NESTED = frozenset({ + ".shipgate/openshell.json", ".claude/hooks/hooks.json", ".codex/config.toml", ".codex/hooks.json",