@occasiolabs/occasio 0.8.1 → 0.8.2
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.
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@occasiolabs/occasio",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.2",
|
|
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/index.js
CHANGED
|
@@ -48,7 +48,13 @@ 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
|