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
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"name": "agentic-control-plane",
"source": "./",
"description": "Control, audit, and cost-optimize every Claude Code tool call. Governance hook + bundled ACP MCP (cost X-ray, run traces, policy checks) + /cost-xray pre-ship report.",
"version": "0.12.0",
"version": "0.13.0",
"author": {
"name": "GatewayStack"
},
Expand Down
20 changes: 18 additions & 2 deletions bin/govern.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ const ACP_GOVERN =
process.env.ACP_API_BASE ||
"https://govern.agenticcontrolplane.com";

const PLUGIN_VERSION = "0.12.0";
const PLUGIN_VERSION = "0.13.0";

// Console base for user-facing deep links (session receipt, #606).
const ACP_CONSOLE =
Expand Down Expand Up @@ -842,7 +842,7 @@ async function handleSessionStart() {
const hookHash = sha256FileHex(fileURLToPath(import.meta.url));
if (!hookHash) process.exit(0);
const grantsHash = sha256FileHex(join(homedir(), ".acp", "harness-grants.json"));
await fetch(`${ACP_GOVERN}/govern/attest`, {
const res = await fetch(`${ACP_GOVERN}/govern/attest`, {
method: "POST",
headers,
body: JSON.stringify({
Expand All @@ -857,6 +857,22 @@ async function handleSessionStart() {
}),
signal: controller.signal,
});
// Upgrade notice (gatewaystack-connect#849): the attest response may
// carry a `notice` when this plugin version is behind the registry's
// latest — surfaced as SessionStart additionalContext so the MODEL
// sees it and can drive the upgrade (behind a human approval; the
// canonical installer is step_up-gated at the gateway). Once per
// session by construction: attest runs at SessionStart only.
// Canonical logic in lib/attestation.mjs (attestNoticeOutput).
const data = await res.json().catch(() => null);
if (data && typeof data.notice === "string" && data.notice.trim()) {
process.stdout.write(JSON.stringify({
hookSpecificOutput: {
hookEventName: "SessionStart",
additionalContext: data.notice.trim(),
},
}));
}
} catch {
// silent — absence of attestation is visible server-side by design
} finally {
Expand Down
18 changes: 18 additions & 0 deletions lib/attestation.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,21 @@ export function buildAttestationPayload({
harness,
};
}

/** Upgrade-notice contract (gatewaystack-connect#849): the /govern/attest
* response may carry `notice` when the reporting plugin version is behind
* the registry's latest. Returns the ONE stdout JSON object SessionStart
* may write, or null. additionalContext reaches the model; a stale hook
* that can't render this is exactly the population the server-side
* console badge and founder-alert enrichment exist for. */
export function attestNoticeOutput(responseBody) {
if (!responseBody || typeof responseBody.notice !== "string") return null;
const notice = responseBody.notice.trim();
if (!notice) return null;
return {
hookSpecificOutput: {
hookEventName: "SessionStart",
additionalContext: notice,
},
};
}
2 changes: 1 addition & 1 deletion plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "agentic-control-plane",
"version": "0.12.0",
"version": "0.13.0",
"description": "Identity, governance, and audit for every Claude Code tool call. Logs all tool usage, enforces policies, and gives teams full visibility \u2014 without changing how you use Claude.",
"author": {
"name": "GatewayStack",
Expand Down
50 changes: 50 additions & 0 deletions skills/acp-upgrade/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
---
name: acp-upgrade
description: Upgrade the ACP governance plugin and hook to the latest version via the canonical installer (human-approved)
user-invocable: true
---

# ACP upgrade

Use this when ACP reports that a newer plugin version is available (an
`[ACP]` upgrade notice at session start), or when the user asks to upgrade
ACP.

## Why upgrades matter

The govern hook is the enforcement point on this machine. Old versions can
carry enforcement gaps — for example, pre-0.11.0 hooks could time out under
gateway latency, and a timed-out PreToolUse hook is non-blocking: the call
runs even though the server issued a deny. Upgrading closes gaps that are
already fixed upstream.

## How to upgrade

1. Run the canonical installer, exactly:

```bash
curl -fsSL https://agenticcontrolplane.com/install.sh | sh
```

Expect an approval requirement: refreshing the governance hook is
step_up-gated at unattended tiers, and the deny reason carries the
approval link. Ask your human to approve it in the ACP console, then
run the SAME command again — the consumed approval lets it through.
Do not modify the command, add flags, or chain other commands onto it:
only the exact canonical form is recognized.

2. After the installer finishes, tell the user to **restart their session**
(the running session keeps the old hook in memory).

3. On the next session start the hook re-attests: the console session view
shows the new version and hash. If the badge still shows the old
version, the installer did not take — check `~/.acp/govern.mjs` was
rewritten and `claude plugin list` shows the current plugin version.

## Never

- Never edit `~/.acp/govern.mjs`, `~/.claude/settings.json` hooks, or the
plugin files directly — governance machinery is human-only; the
canonical installer behind a human approval is the ONE sanctioned path.
- Never work around a denied installer run — the approval link in the
deny reason is the path forward.
17 changes: 17 additions & 0 deletions test/attestation.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,20 @@ test("an attestation without a hook hash is no attestation at all", () => {
null,
);
});

import { attestNoticeOutput } from "../lib/attestation.mjs";

test("upgrade notice from the attest response becomes SessionStart additionalContext", () => {
const out = attestNoticeOutput({ ok: true, verdict: "attested", notice: " [ACP] v0.13.0 available " });
assert.deepEqual(out, {
hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: "[ACP] v0.13.0 available" },
});
});

test("no notice, empty notice, or malformed response → no stdout object", () => {
assert.equal(attestNoticeOutput({ ok: true, verdict: "attested" }), null);
assert.equal(attestNoticeOutput({ notice: " " }), null);
assert.equal(attestNoticeOutput({ notice: 42 }), null);
assert.equal(attestNoticeOutput(null), null);
assert.equal(attestNoticeOutput(undefined), null);
});
Loading