@occasiolabs/occasio 0.8.1 → 0.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE +4 -4
- package/bin/supervisor/README.md +2 -2
- package/bin/supervisor/com.occasio.proxy.plist.template +3 -3
- package/bin/supervisor/install-windows-task.ps1 +7 -7
- package/bin/supervisor/occasio.service +1 -1
- package/package.json +4 -4
- package/src/attest/check-summary.js +115 -0
- package/src/demo/attest-demo.js +1 -1
- package/src/harness.js +6 -6
- package/src/index.js +9 -3
- package/src/mcp-server.js +1 -1
- package/src/policy/loader.js +2 -2
- package/src/redteam.js +1 -1
package/NOTICE
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
Occasio
|
|
2
2
|
Copyright 2026 Leonard Brauer
|
|
3
3
|
|
|
4
|
-
This product includes software developed by the
|
|
5
|
-
(https://github.com/
|
|
4
|
+
This product includes software developed by the Occasio project
|
|
5
|
+
(https://github.com/occasiolabs/occasio).
|
|
6
6
|
|
|
7
7
|
Licensed under the Apache License, Version 2.0 (see LICENSE).
|
|
8
8
|
|
|
9
|
-
Versions 0.6.6 and earlier of
|
|
9
|
+
Versions 0.6.6 and earlier of Occasio were released under the MIT License
|
|
10
10
|
and remain available under MIT in perpetuity for those releases.
|
package/bin/supervisor/README.md
CHANGED
|
@@ -38,12 +38,12 @@ rm ~/.config/systemd/user/occasio.service
|
|
|
38
38
|
|
|
39
39
|
## macOS (launchd, user scope)
|
|
40
40
|
|
|
41
|
-
The plist is a template: replace `{{
|
|
41
|
+
The plist is a template: replace `{{OCCASIO_BIN}}` with the absolute
|
|
42
42
|
path to your `occasio` binary first.
|
|
43
43
|
|
|
44
44
|
```sh
|
|
45
45
|
LF_BIN="$(command -v occasio)"
|
|
46
|
-
sed "s|{{
|
|
46
|
+
sed "s|{{OCCASIO_BIN}}|$LF_BIN|g" com.occasio.proxy.plist.template \
|
|
47
47
|
> ~/Library/LaunchAgents/ai.occasio.proxy.plist
|
|
48
48
|
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/ai.occasio.proxy.plist
|
|
49
49
|
launchctl print gui/$(id -u)/ai.occasio.proxy
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
2
|
<!--
|
|
3
|
-
|
|
3
|
+
Occasio launchd template (v0.6.4).
|
|
4
4
|
|
|
5
|
-
This file is a TEMPLATE: replace {{
|
|
5
|
+
This file is a TEMPLATE: replace {{OCCASIO_BIN}} with the absolute
|
|
6
6
|
path to your `occasio` executable before installing. See
|
|
7
7
|
bin/supervisor/README.md for the install command.
|
|
8
8
|
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
<key>ProgramArguments</key>
|
|
20
20
|
<array>
|
|
21
|
-
<string>{{
|
|
21
|
+
<string>{{OCCASIO_BIN}}</string>
|
|
22
22
|
<string>start</string>
|
|
23
23
|
</array>
|
|
24
24
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
the current user, restarting it within 30 seconds if it exits.
|
|
5
5
|
|
|
6
6
|
.DESCRIPTION
|
|
7
|
-
v0.6.4 of
|
|
7
|
+
v0.6.4 of Occasio aborts with exit code 1 when it cannot append to
|
|
8
8
|
its audit log. This task brings the proxy back up so the agent can
|
|
9
9
|
resume work as soon as the underlying I/O issue clears.
|
|
10
10
|
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
Manually validated on Windows 11 Pro (PowerShell 7.x).
|
|
13
13
|
Tested-on: Windows.
|
|
14
14
|
|
|
15
|
-
The task runs at user logon, not at boot, because
|
|
15
|
+
The task runs at user logon, not at boot, because Occasio's audit
|
|
16
16
|
log lives in the user profile (~/.occasio/). Run from an elevated
|
|
17
17
|
shell only if you need the task to survive logoff.
|
|
18
18
|
#>
|
|
@@ -35,14 +35,14 @@ $Principal = New-ScheduledTaskPrincipal `
|
|
|
35
35
|
-RunLevel Limited
|
|
36
36
|
|
|
37
37
|
Register-ScheduledTask `
|
|
38
|
-
-TaskName "
|
|
39
|
-
-Description "
|
|
38
|
+
-TaskName "Occasio" `
|
|
39
|
+
-Description "Occasio — local AI-agent governance proxy (v0.6.4)" `
|
|
40
40
|
-Action $Action `
|
|
41
41
|
-Trigger $Trigger `
|
|
42
42
|
-Settings $Settings `
|
|
43
43
|
-Principal $Principal `
|
|
44
44
|
-Force
|
|
45
45
|
|
|
46
|
-
Write-Host "Registered scheduled task '
|
|
47
|
-
Write-Host "It will start at next logon. To start now: Start-ScheduledTask -TaskName
|
|
48
|
-
Write-Host "To remove: Unregister-ScheduledTask -TaskName
|
|
46
|
+
Write-Host "Registered scheduled task 'Occasio'."
|
|
47
|
+
Write-Host "It will start at next logon. To start now: Start-ScheduledTask -TaskName Occasio"
|
|
48
|
+
Write-Host "To remove: Unregister-ScheduledTask -TaskName Occasio -Confirm:`$false"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@occasiolabs/occasio",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.3",
|
|
4
4
|
"description": "Occasio — cryptographically verifiable behavioral attestation for AI coding agents. Tool-call interception + policy enforcement + tamper-evident audit chain + Sigstore-signed in-toto attestations + windowed EDR detection. Same engine for Claude Code and MCP; Computer-Use scaffold included.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"files": [
|
|
@@ -47,8 +47,8 @@
|
|
|
47
47
|
"computer-use"
|
|
48
48
|
],
|
|
49
49
|
"bin": {
|
|
50
|
-
"occasio":
|
|
51
|
-
"oc":
|
|
50
|
+
"occasio": "bin/occasio.js",
|
|
51
|
+
"oc": "bin/occasio.js",
|
|
52
52
|
"occasio-mcp": "bin/occasio-mcp.js"
|
|
53
53
|
},
|
|
54
54
|
"engines": {
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
},
|
|
57
57
|
"repository": {
|
|
58
58
|
"type": "git",
|
|
59
|
-
"url": "https://github.com/occasiolabs/occasio.git"
|
|
59
|
+
"url": "git+https://github.com/occasiolabs/occasio.git"
|
|
60
60
|
},
|
|
61
61
|
"homepage": "https://github.com/occasiolabs/occasio#readme",
|
|
62
62
|
"bugs": {
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* check-summary.js — pure function that turns an attestation predicate into
|
|
5
|
+
* the GitHub Check Run body (title + Markdown summary) used by the reference
|
|
6
|
+
* Action. Lives here in `src/` so both the Action (integrations/) and the
|
|
7
|
+
* `occasio demo attest` CLI can import it without crossing the src/ ←
|
|
8
|
+
* integrations/ boundary.
|
|
9
|
+
*
|
|
10
|
+
* No I/O, no env access, no side effects. Safe to call from any context.
|
|
11
|
+
*
|
|
12
|
+
* Why it's pulled out of integrations/attest-action/scripts/post-check.js:
|
|
13
|
+
* integrations/ is deliberately excluded from the npm tarball — it ships
|
|
14
|
+
* as a GitHub Action artifact, not as part of the CLI package. Requiring
|
|
15
|
+
* ../../integrations/... from src/demo/ worked in the source tree but
|
|
16
|
+
* broke immediately on `npm install` because the path doesn't exist in
|
|
17
|
+
* the published tarball. Pure helpers belong in src/ where they ship
|
|
18
|
+
* with the package; the Action imports them from there.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
// ── Markdown escaping ───────────────────────────────────────────────────────
|
|
22
|
+
// Values from the attestation come from the agent's tool calls — a malicious
|
|
23
|
+
// or compromised tool name can contain Markdown control characters that
|
|
24
|
+
// break out of the table cell, inject links, or smuggle raw HTML through
|
|
25
|
+
// GitHub's renderer. Escape before interpolating into the Check Run summary.
|
|
26
|
+
//
|
|
27
|
+
// `mdCode` is for values rendered inside `` `...` `` cells: only the
|
|
28
|
+
// backtick itself needs handling (we strip rather than escape, because
|
|
29
|
+
// inline-code in GitHub-Flavored Markdown has no escape mechanism).
|
|
30
|
+
function mdCode(s) {
|
|
31
|
+
if (s === null || s === undefined) return '';
|
|
32
|
+
return String(s)
|
|
33
|
+
.replace(/[\r\n]+/g, ' ') // collapse newlines so cells stay one-line
|
|
34
|
+
.replace(/`/g, 'ˋ') // backtick → ˋ (visually similar, breaks out impossible)
|
|
35
|
+
.replace(/\|/g, '\\|') // GFM table cell separator
|
|
36
|
+
.slice(0, 256); // truncate runaway values
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// `mdText` is for values rendered as raw Markdown text (no code fence
|
|
40
|
+
// around them). Escape the full GFM punctuation set.
|
|
41
|
+
function mdText(s) {
|
|
42
|
+
if (s === null || s === undefined) return '';
|
|
43
|
+
return String(s)
|
|
44
|
+
.replace(/[\r\n]+/g, ' ')
|
|
45
|
+
.replace(/([\\`*_{}\[\]()#+\-.!|<>])/g, '\\$1')
|
|
46
|
+
.slice(0, 256);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function intOr0(n) { return Number.isFinite(+n) ? Math.trunc(+n) : 0; }
|
|
50
|
+
|
|
51
|
+
// Only render Rekor as a link if it is the public Sigstore search domain
|
|
52
|
+
// over https. Anything else → display as escaped text, no clickable link.
|
|
53
|
+
function safeRekorUrl(raw) {
|
|
54
|
+
if (typeof raw !== 'string') return null;
|
|
55
|
+
try {
|
|
56
|
+
const u = new URL(raw);
|
|
57
|
+
if (u.protocol !== 'https:') return null;
|
|
58
|
+
if (u.host !== 'search.sigstore.dev') return null;
|
|
59
|
+
return u.toString();
|
|
60
|
+
} catch { return null; }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ── Summary builder ─────────────────────────────────────────────────────────
|
|
64
|
+
// Pure function — no env access, no I/O — so it can be unit-tested directly.
|
|
65
|
+
// Returns { title, summary, signed } ready for the Checks API body.
|
|
66
|
+
function buildSummary(att, rekorRaw) {
|
|
67
|
+
const sum = att.execution_summary || {};
|
|
68
|
+
const signed = !!(att.signature && att.signature.type);
|
|
69
|
+
|
|
70
|
+
const title = String(
|
|
71
|
+
`Occasio Attested · ${intOr0(sum.tool_calls)} calls · ${intOr0(sum.blocked)} blocked`
|
|
72
|
+
).slice(0, 255);
|
|
73
|
+
|
|
74
|
+
const lines = [];
|
|
75
|
+
lines.push(`**Predicate:** \`${mdCode(att.predicate_type)}\``);
|
|
76
|
+
lines.push('');
|
|
77
|
+
lines.push(`| Field | Value |`);
|
|
78
|
+
lines.push(`|---|---|`);
|
|
79
|
+
lines.push(`| Agent | \`${mdCode(att.agent?.platform || 'unknown')}\` ${att.agent?.model ? '· `' + mdCode(att.agent.model) + '`' : ''} |`);
|
|
80
|
+
lines.push(`| run_id | \`${mdCode(att.subject?.run_id || 'unknown')}\` |`);
|
|
81
|
+
if (att.subject?.git_commit) {
|
|
82
|
+
lines.push(`| commit | \`${mdCode(String(att.subject.git_commit).slice(0,12))}\` |`);
|
|
83
|
+
}
|
|
84
|
+
lines.push(`| Policy hash | \`${mdCode((att.policy?.file_hash || '').slice(0,16))}…\` (${mdText(att.policy?.source || 'unknown')}) |`);
|
|
85
|
+
lines.push(`| Tool calls | **${intOr0(sum.tool_calls)}** (LOCAL ${intOr0(sum.local)} · PASS ${intOr0(sum.passed)} · BLOCK ${intOr0(sum.blocked)} · TRANSFORM ${intOr0(sum.transformed)}) |`);
|
|
86
|
+
lines.push(`| Secrets redacted | ${intOr0(sum.secrets_redacted)} |`);
|
|
87
|
+
lines.push(`| Chain | \`${mdCode((att.audit_chain?.first_hash || '').slice(0,12) || '∅')}…${mdCode((att.audit_chain?.last_hash || '').slice(0,12) || '∅')}\` · ${intOr0(att.audit_chain?.event_count)} events |`);
|
|
88
|
+
lines.push(`| Signature | ${signed ? `✓ \`${mdCode(att.signature.type)}\`` : '— unsigned (informational only)'} |`);
|
|
89
|
+
const rekorSafe = safeRekorUrl(rekorRaw);
|
|
90
|
+
if (rekorSafe) lines.push(`| Rekor | [search.sigstore.dev](${rekorSafe}) |`);
|
|
91
|
+
else if (rekorRaw) lines.push(`| Rekor | \`${mdCode(rekorRaw)}\` (non-Rekor URL — not linked) |`);
|
|
92
|
+
lines.push('');
|
|
93
|
+
|
|
94
|
+
if (Array.isArray(sum.blocked_events) && sum.blocked_events.length) {
|
|
95
|
+
lines.push('### Blocked attempts');
|
|
96
|
+
lines.push('');
|
|
97
|
+
lines.push('| Tool | Target | Rule | When |');
|
|
98
|
+
lines.push('|---|---|---|---|');
|
|
99
|
+
for (const b of sum.blocked_events.slice(0, 25)) {
|
|
100
|
+
lines.push(`| \`${mdCode(b.tool)}\` | \`${mdCode((b.target || '').slice(0, 80))}\` | \`${mdCode(b.rule)}\` | T+${intOr0(b.at_offset_s)}s |`);
|
|
101
|
+
}
|
|
102
|
+
if (sum.blocked_events.length > 25) {
|
|
103
|
+
lines.push(`| … | (${intOr0(sum.blocked_events.length - 25)} more in artifact) | | |`);
|
|
104
|
+
}
|
|
105
|
+
lines.push('');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
lines.push(`<sub>Spec: <a href="https://github.com/occasiolabs/occasio/blob/main/spec/agent-attestation/v1/README.md">agent-attestation/v1</a> · Independent verifier: <code>occasio attest verify</code> · Artifacts attached to this workflow run.</sub>`);
|
|
109
|
+
|
|
110
|
+
return { title, summary: lines.join('\n'), signed };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
module.exports = {
|
|
114
|
+
mdCode, mdText, intOr0, safeRekorUrl, buildSummary,
|
|
115
|
+
};
|
package/src/demo/attest-demo.js
CHANGED
|
@@ -39,7 +39,7 @@ const crypto = require('crypto');
|
|
|
39
39
|
const { buildAttestation } = require('../attest');
|
|
40
40
|
const { verifyFile } = require('../audit/verifier');
|
|
41
41
|
const { canonicalize } = require('../attest/canonicalize');
|
|
42
|
-
const { buildSummary } = require('
|
|
42
|
+
const { buildSummary } = require('../attest/check-summary');
|
|
43
43
|
|
|
44
44
|
const C = {
|
|
45
45
|
r: s => `\x1b[31m${s}\x1b[0m`,
|
package/src/harness.js
CHANGED
|
@@ -538,9 +538,9 @@ function runScenarioChild(scenarioName, ctx, opts = {}) {
|
|
|
538
538
|
|
|
539
539
|
const env = {
|
|
540
540
|
...process.env,
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
541
|
+
OCCASIO_PORT: String(port),
|
|
542
|
+
OCCASIO_AUDIT_FILE: ctx.auditPath,
|
|
543
|
+
OCCASIO_POLICY_FILE: ctx.policyPath,
|
|
544
544
|
};
|
|
545
545
|
// Only set ANTHROPIC_API_KEY if we actually have one. Empty/undefined
|
|
546
546
|
// would override the user's Claude Code bundled auth, which is the
|
|
@@ -598,8 +598,8 @@ function runMcpScenario(scenarioName, ctx, opts = {}) {
|
|
|
598
598
|
return new Promise((resolve) => {
|
|
599
599
|
const env = {
|
|
600
600
|
...process.env,
|
|
601
|
-
|
|
602
|
-
|
|
601
|
+
OCCASIO_AUDIT_FILE: ctx.auditPath,
|
|
602
|
+
OCCASIO_POLICY_FILE: ctx.policyPath,
|
|
603
603
|
};
|
|
604
604
|
const child = spawnFn('node', [mcpBin], {
|
|
605
605
|
cwd: ctx.workspace, env, stdio: ['pipe', 'pipe', 'pipe'],
|
|
@@ -749,7 +749,7 @@ async function runHarness(opts = {}) {
|
|
|
749
749
|
v.workspace = ctx.workspace;
|
|
750
750
|
results.push(v);
|
|
751
751
|
} finally {
|
|
752
|
-
if (!opts.keepScratch && !process.env.
|
|
752
|
+
if (!opts.keepScratch && !process.env.OCC_HARNESS_KEEP) {
|
|
753
753
|
cleanupWorkspace(ctx);
|
|
754
754
|
}
|
|
755
755
|
}
|
package/src/index.js
CHANGED
|
@@ -48,12 +48,18 @@ const { runInspectCli } = require('./inspect');
|
|
|
48
48
|
const { runAuditCli } = require('./audit/verifier');
|
|
49
49
|
const { budgetStatus, fmtBudget, BUDGET_EXCEEDED_EVENT } = require('./budget');
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
// Source of truth: package.json. Read at startup so `occasio --version`
|
|
52
|
+
// can't drift from what npm reports — the previous hardcoded constant
|
|
53
|
+
// caused 0.8.1's CLI to mis-report itself as 0.8.0.
|
|
54
|
+
const VERSION = (() => {
|
|
55
|
+
try { return require('../package.json').version; }
|
|
56
|
+
catch { return '0.0.0-unknown'; }
|
|
57
|
+
})();
|
|
52
58
|
const LOG_SCHEMA_VERSION = 2;
|
|
53
59
|
// Port override via env var (used by `occasio harness` and redteam to
|
|
54
60
|
// run isolated proxies against scratch audit chains on free ports). Default
|
|
55
61
|
// is 8081 to preserve existing user-facing behaviour.
|
|
56
|
-
let PORT = parseInt(process.env.
|
|
62
|
+
let PORT = parseInt(process.env.OCCASIO_PORT, 10) || 8081;
|
|
57
63
|
const ANTHROPIC_REAL = 'api.anthropic.com';
|
|
58
64
|
const LOG_DIR = path.join(os.homedir(), '.occasio');
|
|
59
65
|
const SESSION_FILE = path.join(LOG_DIR, 'session.json');
|
|
@@ -920,7 +926,7 @@ const { createAuditor: _createAuditor } = require('./audit/jsonl-auditor');
|
|
|
920
926
|
// Audit-file override via env var. Used by `occasio harness` to run
|
|
921
927
|
// against a scratch chain so the user's real ~/.occasio/pipeline-events
|
|
922
928
|
// .jsonl is never touched. When unset, the auditor uses its default location.
|
|
923
|
-
const sessionAuditor = _createAuditor(process.env.
|
|
929
|
+
const sessionAuditor = _createAuditor(process.env.OCCASIO_AUDIT_FILE || undefined);
|
|
924
930
|
|
|
925
931
|
// v0.6.6: register a policy-change listener that emits a `policy_loaded`
|
|
926
932
|
// audit row whenever the active policy hash transitions to a new value
|
package/src/mcp-server.js
CHANGED
|
@@ -44,7 +44,7 @@ const LOG_FILE = path.join(os.homedir(), '.occasio', 'mcp-experiment.jsonl');
|
|
|
44
44
|
// Audit-file override via env var (symmetric with the Claude Code proxy
|
|
45
45
|
// in src/index.js). Used by `occasio harness --scenario mcp-*` to
|
|
46
46
|
// keep MCP test traffic out of the user's real ~/.occasio chain.
|
|
47
|
-
let mcpAuditor = createAuditor(process.env.
|
|
47
|
+
let mcpAuditor = createAuditor(process.env.OCCASIO_AUDIT_FILE || undefined);
|
|
48
48
|
|
|
49
49
|
// v0.6.6: emit a policy_loaded row on first policy load and on every
|
|
50
50
|
// hot-reload that changes the policy file's bytes. The MCP server is a
|
package/src/policy/loader.js
CHANGED
|
@@ -26,10 +26,10 @@ function resolveConfigPath(p) {
|
|
|
26
26
|
return path.resolve(expanded);
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
// Default path can be overridden via
|
|
29
|
+
// Default path can be overridden via OCCASIO_POLICY_FILE — used by the
|
|
30
30
|
// harness/redteam commands to point the proxy at a scratch policy.yml so
|
|
31
31
|
// the user's real ~/.occasio/policy.yml is never read.
|
|
32
|
-
const DEFAULT_PATH = process.env.
|
|
32
|
+
const DEFAULT_PATH = process.env.OCCASIO_POLICY_FILE
|
|
33
33
|
|| path.join(os.homedir(), '.occasio', 'policy.yml');
|
|
34
34
|
|
|
35
35
|
// Default tool routing matches the pre-Stage-3 hardcoded behavior.
|
package/src/redteam.js
CHANGED
|
@@ -410,7 +410,7 @@ async function runRedteamCli(args = []) {
|
|
|
410
410
|
process.stdout.write('\n');
|
|
411
411
|
return result;
|
|
412
412
|
} finally {
|
|
413
|
-
if (!keepScratch && !process.env.
|
|
413
|
+
if (!keepScratch && !process.env.OCC_REDTEAM_KEEP) {
|
|
414
414
|
try { fs.rmSync(ctx.workspace, { recursive: true, force: true }); } catch {}
|
|
415
415
|
}
|
|
416
416
|
}
|