@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 CHANGED
@@ -1,10 +1,10 @@
1
- LocalFirst
1
+ Occasio
2
2
  Copyright 2026 Leonard Brauer
3
3
 
4
- This product includes software developed by the LocalFirst project
5
- (https://github.com/occasio-ai/occasio).
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 LocalFirst were released under the MIT License
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.
@@ -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 `{{LOCALFIRST_BIN}}` with the absolute
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|{{LOCALFIRST_BIN}}|$LF_BIN|g" com.occasio.proxy.plist.template \
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
- LocalFirst launchd template (v0.6.4).
3
+ Occasio launchd template (v0.6.4).
4
4
 
5
- This file is a TEMPLATE: replace {{LOCALFIRST_BIN}} with the absolute
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>{{LOCALFIRST_BIN}}</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 LocalFirst aborts with exit code 1 when it cannot append to
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 LocalFirst's audit
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 "LocalFirst" `
39
- -Description "LocalFirst — local AI-agent governance proxy (v0.6.4)" `
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 'LocalFirst'."
47
- Write-Host "It will start at next logon. To start now: Start-ScheduledTask -TaskName LocalFirst"
48
- Write-Host "To remove: Unregister-ScheduledTask -TaskName LocalFirst -Confirm:`$false"
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"
@@ -1,5 +1,5 @@
1
1
  [Unit]
2
- Description=LocalFirst — local AI-agent governance proxy
2
+ Description=Occasio — local AI-agent governance proxy
3
3
  After=network-online.target
4
4
  Wants=network-online.target
5
5
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@occasiolabs/occasio",
3
- "version": "0.8.1",
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": "bin/occasio.js",
51
- "oc": "bin/occasio.js",
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
+ };
@@ -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('../../integrations/attest-action/scripts/post-check');
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
- LOCALFIRST_PORT: String(port),
542
- LOCALFIRST_AUDIT_FILE: ctx.auditPath,
543
- LOCALFIRST_POLICY_FILE: ctx.policyPath,
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
- LOCALFIRST_AUDIT_FILE: ctx.auditPath,
602
- LOCALFIRST_POLICY_FILE: ctx.policyPath,
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.LF_HARNESS_KEEP) {
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
- const VERSION = '0.8.0';
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.LOCALFIRST_PORT, 10) || 8081;
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.LOCALFIRST_AUDIT_FILE || undefined);
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.LOCALFIRST_AUDIT_FILE || undefined);
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
@@ -26,10 +26,10 @@ function resolveConfigPath(p) {
26
26
  return path.resolve(expanded);
27
27
  }
28
28
 
29
- // Default path can be overridden via LOCALFIRST_POLICY_FILE — used by the
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.LOCALFIRST_POLICY_FILE
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.LF_REDTEAM_KEEP) {
413
+ if (!keepScratch && !process.env.OCC_REDTEAM_KEEP) {
414
414
  try { fs.rmSync(ctx.workspace, { recursive: true, force: true }); } catch {}
415
415
  }
416
416
  }