Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .pre-commit-hooks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
(.*/)?\.claude/(settings(\.local)?\.json|commands(/.*)?|hooks/hooks\.json)|
(.*/)?\.cursor/(cli\.json|mcp\.json|rules(/.*)?)|
(.*/)?\.vscode/mcp\.json|
(.*/)?\.shipgate/agent-contract\.json|
(.*/)?\.shipgate/(agent-contract|openshell)\.json|
(.*/)?(AGENTS(\.override)?|CLAUDE)\.md|
\.(agents|claude)/skills/.*|
(.*/)?\.codex-plugin(/.*)?|
Expand Down Expand Up @@ -112,7 +112,7 @@
(.*/)?\.claude/(settings(\.local)?\.json|commands(/.*)?|hooks/hooks\.json)|
(.*/)?\.cursor/(cli\.json|mcp\.json|rules(/.*)?)|
(.*/)?\.vscode/mcp\.json|
(.*/)?\.shipgate/agent-contract\.json|
(.*/)?\.shipgate/(agent-contract|openshell)\.json|
(.*/)?(AGENTS(\.override)?|CLAUDE)\.md|
\.(agents|claude)/skills/.*|
(.*/)?\.codex-plugin(/.*)?|
Expand Down
13 changes: 9 additions & 4 deletions docs/checks.json
Original file line number Diff line number Diff line change
Expand Up @@ -2090,17 +2090,22 @@
"autofix_safe": false,
"category": "host_boundary",
"default_severity": "high",
"description": "The Claude Code permission allowlist expanded.",
"description": "The Claude Code permission allowlist expanded. Also reports proven expansion of an explicitly selected OpenShell policy.",
"docs_url": "https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/docs/checks.md#ship-host-boundary-permission-allow-expanded",
"dynamic_default": false,
"evidence_fields": [
"direction",
"explanation",
"kind",
"rule",
"limits",
"mode",
"narrowed",
"rule",
"setting",
"value"
"value",
"widened"
],
"fires_when": "A changed .claude/settings.json or .claude/settings.local.json adds a non-wildcard permissions.allow entry. It also fires when the change sets any other value of a Claude Code setting the host-grant table models - a 'defaultMode' such as 'acceptEdits' or 'dontAsk', an 'enabledMcpjsonServers' entry, 'disableBypassPermissionsMode', 'disableAllHooks', the managed-only switches, or 'false' for the prompt switches - at the rating the table gives the value, which is the rating its grant and host-diff row carry (#827).",
"fires_when": "A changed .claude/settings.json or .claude/settings.local.json adds a non-wildcard permissions.allow entry. It also fires when the change sets any other value of a Claude Code setting the host-grant table models - a 'defaultMode' such as 'acceptEdits' or 'dontAsk', an 'enabledMcpjsonServers' entry, 'disableBypassPermissionsMode', 'disableAllHooks', the managed-only switches, or 'false' for the prompt switches - at the rating the table gives the value, which is the rating its grant and host-diff row carry (#827). OpenShell declared filesystem, Landlock or supported network authority expands, including removal of REST enforcement.",
"floor_severity": "medium",
"id": "SHIP-HOST-BOUNDARY-PERMISSION-ALLOW-EXPANDED",
"mvp_tier": "lifecycle",
Expand Down
2 changes: 1 addition & 1 deletion docs/checks.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ baseline summary and do not fail CI.
| `SHIP-HOST-BOUNDARY-MCP-SERVER-ADDED` | high | A new MCP server was declared for the coding-agent host. |
| `SHIP-HOST-BOUNDARY-MCP-SERVER-CHANGED` | high | An existing MCP server declaration changed its command, URL, args, or env keys. |
| `SHIP-HOST-BOUNDARY-PERMISSION-WILDCARD-ALLOW` | critical | A Claude Code allow rule grants a wildcard surface over a tool that can execute, reach the network, or write, or a setting removes a prompt wholesale. |
| `SHIP-HOST-BOUNDARY-PERMISSION-ALLOW-EXPANDED` | high | The Claude Code permission allowlist expanded, by a scoped rule or a read-only wildcard, or a modelled setting changed, at the setting's rating. |
| `SHIP-HOST-BOUNDARY-PERMISSION-ALLOW-EXPANDED` | high | The Claude Code permission allowlist expanded, a modelled setting changed at its rating, or a selected OpenShell policy expanded declared authority. |
| `SHIP-HOST-BOUNDARY-PERMISSION-DENY-REMOVED` | high | A Claude Code permission deny rule was removed. |
| `SHIP-HOST-BOUNDARY-HOOK-CHANGED` | high | Claude Code hooks changed. |
| `SHIP-HOST-BOUNDARY-WORKFLOW-WRITE-ALL` | critical | A GitHub workflow grants write-all permissions. |
Expand Down
34 changes: 22 additions & 12 deletions docs/checks/host_boundary.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -116,18 +116,28 @@ checks:
floor_severity: medium
mvp_tier: lifecycle
requires_human_review: true
description: The Claude Code permission allowlist expanded.
rationale: Every new allow rule widens what the host executes without a
prompt; expansion needs a human in the loop.
fires_when: A changed .claude/settings.json or .claude/settings.local.json
adds a non-wildcard permissions.allow entry. It also fires when the
change sets any other value of a Claude Code setting the host-grant table
models - a 'defaultMode' such as 'acceptEdits' or 'dontAsk', an
'enabledMcpjsonServers' entry, 'disableBypassPermissionsMode',
'disableAllHooks', the managed-only switches, or 'false' for the prompt
switches - at the rating the table gives the value, which is the rating
its grant and host-diff row carry (#827).
evidence_fields: [kind, rule, mode, setting, value]
description: The Claude Code permission allowlist expanded. Also reports proven expansion of an explicitly
selected OpenShell policy.
rationale: Every new allow rule widens what the host executes without a prompt; expansion needs a human
in the loop.
fires_when: A changed .claude/settings.json or .claude/settings.local.json adds a non-wildcard permissions.allow
entry. It also fires when the change sets any other value of a Claude Code setting the host-grant
table models - a 'defaultMode' such as 'acceptEdits' or 'dontAsk', an 'enabledMcpjsonServers' entry,
'disableBypassPermissionsMode', 'disableAllHooks', the managed-only switches, or 'false' for the prompt
switches - at the rating the table gives the value, which is the rating its grant and host-diff row
carry (#827). OpenShell declared filesystem, Landlock or supported network authority expands, including
removal of REST enforcement.
evidence_fields:
- direction
- explanation
- kind
- limits
- mode
- narrowed
- rule
- setting
- value
- widened
recommendation: Have a human approve the new permission allow rule.
- id: SHIP-HOST-BOUNDARY-PERMISSION-DENY-REMOVED
default_severity: high
Expand Down
2 changes: 1 addition & 1 deletion docs/integrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -436,7 +436,7 @@ repos:
(.*/)?\.claude/(settings(\.local)?\.json|commands(/.*)?|hooks/hooks\.json)|
(.*/)?\.cursor/(cli\.json|mcp\.json|rules(/.*)?)|
(.*/)?\.vscode/mcp\.json|
(.*/)?\.shipgate/agent-contract\.json|
(.*/)?\.shipgate/(agent-contract|openshell)\.json|
(.*/)?(AGENTS(\.override)?|CLAUDE)\.md|
\.(agents|claude)/skills/.*|
(.*/)?\.codex-plugin(/.*)?|
Expand Down
56 changes: 53 additions & 3 deletions docs/openshell-support.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,7 @@ Defaults follow the pinned [OpenShell v0.1.2 authored schema](https://github.com
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. Git dependency identity, gate
integration, local composition and native proof are separate implementation
stages (#945–#948).
substitute for upstream policy validation. Local composition and native proof are separate implementation stages (#947–#948).

## Conservative declared-authority comparison

Expand Down Expand Up @@ -146,3 +144,55 @@ Credential-shaped input paths cannot identify published bytes after redaction.
They become named unsupported, unconfirmable inputs, without publishing raw
paths or link-target digests. Static identity establishes which documents were
read, not whether an effective export is fresh or enforced by a running sandbox.

## Local control and verifier routing

`check` evaluates selected OpenShell inputs for Codex, Claude Code and Cursor
callers alike. It reads both compared trees, including selection-only edits,
deleted registrations, arbitrary filenames and link targets. The same policy
comparator supplies `audit --host --drift`, `diff`, `check` and verifier rows.

A proved expansion uses `SHIP-HOST-BOUNDARY-PERMISSION-ALLOW-EXPANDED`. Mixed
changes keep proven expansions. Unknown authority uses the existing protected
surface review rule. Unread/malformed/unsupported inputs use the existing
incomplete-input route, so absent grant rows never authorize completion.
Neutral or proved narrowings can clear only their exact selected document's
obligation; other trust-root, manifest, policy and instruction edits still apply.
Selection edits and changed link identities retain separate review obligations.

Trigger evaluation takes exact selected paths as explicit context, so even a
selected `README.md` overrides a docs-only skip. Preflight protects selected
inputs as exact host trust roots, including ignored files; the trust graph
binds their captured identity without treating filenames as globs.

With a configured application gate, verifier findings project to the normal
release decision, control permissions, Markdown and SARIF. Without one,
`verify` remains an advisory host comparison and routes to `audit --host`;
it requires no invented purpose, action effects, authority or agent bindings,
and establishes no application merge verdict. Preview never grants completion.

For an end-to-end exercise, register the sample policy, commit it as the base,
then remove `enforcement: enforce` from its REST endpoint. Run:

```bash
shipgate audit --host --workspace . --json
shipgate diff --workspace . --base main --json
shipgate check --agent codex --workspace . --base main --format agent-boundary-json
shipgate verify --workspace . --base main --json
shipgate agent control --workspace . --reports-dir agents-shipgate-reports
```

The change widens well-formed REST request authority by restoring audit mode.
Review the existing control route and its permissions. A subsequent selected
policy edit invalidates the receipt/current control, including ignored paths.
The route-parity fixtures exercise this transition for all three callers,
configured verification and SARIF, plus malformed input and Git-tree isolation.
Validation is pinned to OpenShell v0.1.2/schema 1 and the supported comparison
subset above. Gateway state, live enforcement, credentials, provider/global
composition and native containment remain outside this static MVP.

The pre-commit hook recognizes OpenShell selection files. Its static filename
filter cannot identify an arbitrary selected policy path on its own. Run
`shipgate check` or `agents-shipgate verify` for those changes, or set
`always_run: true` on the local hook. The GitHub verifier reads the explicit
selection and evaluates its dependencies.
11 changes: 11 additions & 0 deletions docs/triggers.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,17 @@
}
],
"rules": [
{
"id": "TRIGGER-OPENSHELL-BOUNDARY-CHANGED",
"surface_class": "host_boundary",
"agents_md_row": "Adds/changes coding-agent host config, hooks, permissions, MCP servers, or workflows",
"when": {
"boundary_adapter": "openshell"
},
"action": "run_shipgate",
"rationale": "Explicit OpenShell policy selections define a reviewed sandbox authority surface.",
"command": "agents-shipgate verify --preview --json"
},
{
"id": "TRIGGER-MCP-EXPORT-CHANGED",
"surface_class": "capability",
Expand Down
2 changes: 1 addition & 1 deletion examples/pre-commit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ repos:
(.*/)?\.claude/(settings(\.local)?\.json|commands(/.*)?|hooks/hooks\.json)|
(.*/)?\.cursor/(cli\.json|mcp\.json|rules(/.*)?)|
(.*/)?\.vscode/mcp\.json|
(.*/)?\.shipgate/agent-contract\.json|
(.*/)?\.shipgate/(agent-contract|openshell)\.json|
(.*/)?(AGENTS(\.override)?|CLAUDE)\.md|
\.(agents|claude)/skills/.*|
(.*/)?\.codex-plugin(/.*)?|
Expand Down
2 changes: 1 addition & 1 deletion llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3523,7 +3523,7 @@ baseline summary and do not fail CI.
| `SHIP-HOST-BOUNDARY-MCP-SERVER-ADDED` | high | A new MCP server was declared for the coding-agent host. |
| `SHIP-HOST-BOUNDARY-MCP-SERVER-CHANGED` | high | An existing MCP server declaration changed its command, URL, args, or env keys. |
| `SHIP-HOST-BOUNDARY-PERMISSION-WILDCARD-ALLOW` | critical | A Claude Code allow rule grants a wildcard surface over a tool that can execute, reach the network, or write, or a setting removes a prompt wholesale. |
| `SHIP-HOST-BOUNDARY-PERMISSION-ALLOW-EXPANDED` | high | The Claude Code permission allowlist expanded, by a scoped rule or a read-only wildcard, or a modelled setting changed, at the setting's rating. |
| `SHIP-HOST-BOUNDARY-PERMISSION-ALLOW-EXPANDED` | high | The Claude Code permission allowlist expanded, a modelled setting changed at its rating, or a selected OpenShell policy expanded declared authority. |
| `SHIP-HOST-BOUNDARY-PERMISSION-DENY-REMOVED` | high | A Claude Code permission deny rule was removed. |
| `SHIP-HOST-BOUNDARY-HOOK-CHANGED` | high | Claude Code hooks changed. |
| `SHIP-HOST-BOUNDARY-WORKFLOW-WRITE-ALL` | critical | A GitHub workflow grants write-all permissions. |
Expand Down
7 changes: 7 additions & 0 deletions src/agents_shipgate/checks/verify.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ def run(context: ScanContext) -> list[Finding]:
verification = context.verification
if verification is None:
return []
from agents_shipgate.core.agent_boundary import assessment_for_scan_context

assessment = assessment_for_scan_context(context)
findings: list[Finding] = []
seen: set[str] = set()
for raw in verification.changed_files:
Expand All @@ -58,6 +61,10 @@ def run(context: ScanContext) -> list[Finding]:
continue
seen.add(path)
classification = _configured_manifest(context, path) or _classify(path)
if path in assessment.openshell_safe_paths and classification is None:
continue
if classification is None and path in assessment.openshell_paths:
classification = ("host_boundary", path)
if classification is None:
continue
trust_root_class, matched_glob = classification
Expand Down
4 changes: 4 additions & 0 deletions src/agents_shipgate/cli/agent_result.py
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,10 @@ def _assessment_for_diff(
)
if evidence_issues:
input_issues = [*(input_issues or []), *evidence_issues]
if enabled_plugin_hooks and enabled_plugin_hooks.openshell:
trigger = evaluate_trigger(paths=changed_files, diff_text=diff_text,
manifest_present=manifest_present, user_requested=True,
selected_host_paths=sorted(enabled_plugin_hooks.openshell.paths))
return evaluate_agent_boundary(
workspace=workspace,
diff_text=diff_text,
Expand Down
13 changes: 13 additions & 0 deletions src/agents_shipgate/cli/trigger.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,13 +193,26 @@ def trigger(
)
raise typer.Exit(2) from exc

from agents_shipgate.cli.verify.host_comparison import enabled_plugin_hook_evidence
from agents_shipgate.core.host_grants import build_host_boundary_snapshot

if use_git:
_, files, issues = enabled_plugin_hook_evidence(workspace=workspace.resolve(),
changed_files=paths, head_is_worktree=base is None,
base=base or head or "HEAD", head=(head or "HEAD") if base else None)
selected = sorted(files.openshell.paths) if files and files.openshell else []
else:
selected = sorted(build_host_boundary_snapshot(workspace).cache.openshell_selected_paths)
issues = []
result = evaluate(
paths=paths,
diff_text=diff_text,
manifest_present=manifest_present_resolved,
detect_result=detect_result,
user_requested=user_requested,
triggers=triggers,
selected_host_paths=selected,
input_status="partial" if issues else "complete",
)

if json_output:
Expand Down
34 changes: 29 additions & 5 deletions src/agents_shipgate/cli/verify/host_comparison.py
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,12 @@ def enabled_plugin_hook_evidence(
if not candidates or not base:
return None, None, []
declarations_changed = any(is_boundary_surface_path(path) for path in candidates)
from dataclasses import replace

from agents_shipgate.cli.verify.host_tree import materialize_host_tree
from agents_shipgate.core.openshell_boundary import OpenShellBoundaryEvidence

openshell_sides = []

snapshot: HostBoundarySnapshot | None = None
files = EnabledPluginHookFiles()
Expand All @@ -352,6 +358,10 @@ def enabled_plugin_hook_evidence(
] if declarations_changed else []
snapshot = build_host_boundary_snapshot(workspace, scope="repository")
files = files.union(EnabledPluginHookFiles.of(snapshot))
openshell_sides.append(snapshot)
if snapshot.cache.openshell_selected_paths:
if not commits:
commits.append(commit_sha(workspace, "HEAD") if base == "HEAD" else merge_base_sha(workspace, base, "HEAD"))
else:
head_ref = head or "HEAD"
# Preserve each host's selection from both declaration trees.
Expand All @@ -363,23 +373,37 @@ def enabled_plugin_hook_evidence(
raise ValueError("a compared commit is not available locally")
with tempfile.TemporaryDirectory(prefix="shipgate-plugin-hooks-") as scratch:
tree = Path(scratch) / "tree"
archive_tree(workspace, commit, tree, scope=is_boundary_surface_path)
files = files.union(EnabledPluginHookFiles.of(build_host_boundary_snapshot(
tree, cache=HostStaticParseCache(reference_workspace=workspace),
)))
tree, archived = materialize_host_tree(workspace, commit, tree, archive=archive_tree)
archived = archived or build_host_boundary_snapshot(
tree, cache=HostStaticParseCache(reference_workspace=workspace))
openshell_sides.append(archived)
if not head_is_worktree and len(commits) == 1 and archived.cache.openshell_selected_paths:
commits.append(merge_base_sha(workspace, base, head or "HEAD"))
files = files.union(EnabledPluginHookFiles.of(archived))
except (OSError, RuntimeError, ValueError, ConfigError):
return snapshot, None, [
BoundaryInputIssue(
code="host_inventory_unreadable",
path=path,
message=(
"The hook configuration of a compared commit could not be read, so "
"The selected host inputs of a compared commit could not be read, so "
"whether this file declares selected plugin hooks or is a selected "
"hook executable is not established."
),
)
for path in candidates
]
if openshell_sides:
# Head is read first; the immutable merge base is the final side.
head_side, base_side = openshell_sides[0], openshell_sides[-1]
paths = frozenset(base_side.cache.openshell_selected_paths | head_side.cache.openshell_selected_paths)
before_reads, after_reads = base_side.cache.openshell_input_reads, head_side.cache.openshell_input_reads
links = frozenset(path for path in before_reads.keys() | after_reads.keys()
if any(reads.get(path, {}).get("source") == "generated" for reads in (before_reads, after_reads))
and before_reads.get(path) != after_reads.get(path))
files = replace(files, openshell=OpenShellBoundaryEvidence(paths=paths, changed_links=links,
before=base_side.inventory if len(openshell_sides) > 1 else None,
after=head_side.inventory))
return snapshot, files, []


Expand Down
6 changes: 6 additions & 0 deletions src/agents_shipgate/cli/verify/orchestrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -958,6 +958,12 @@ def run_verify(
)
)

if enabled_plugin_hooks and enabled_plugin_hooks.openshell:
trigger = evaluate(paths=changed_files, diff_text=diff_text,
manifest_present=config_path.exists(), user_requested=True,
input_status=_trigger_input_status(diff_input),
selected_host_paths=sorted(enabled_plugin_hooks.openshell.paths))

report: ReadinessReport | None = None
head_status = "failed"
head_exit_code = 4
Expand Down
Loading
Loading