@occasiolabs/occasio 0.8.1

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.
Files changed (92) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +10 -0
  3. package/README.md +216 -0
  4. package/bin/occasio-mcp.js +5 -0
  5. package/bin/occasio.js +2 -0
  6. package/bin/supervisor/README.md +90 -0
  7. package/bin/supervisor/com.occasio.proxy.plist.template +36 -0
  8. package/bin/supervisor/install-windows-task.ps1 +48 -0
  9. package/bin/supervisor/occasio.service +18 -0
  10. package/docs/AUDIT.md +120 -0
  11. package/docs/attest_verify.py +283 -0
  12. package/docs/audit_walker.py +65 -0
  13. package/docs/canonicalize.py +99 -0
  14. package/docs/compliance-mapping.md +93 -0
  15. package/docs/demos/mcp-block.md +148 -0
  16. package/docs/edr-calibration.md +73 -0
  17. package/docs/edr-demo.md +83 -0
  18. package/docs/python-verifier.md +74 -0
  19. package/docs/reference-pipeline.md +140 -0
  20. package/package.json +69 -0
  21. package/policy-templates/dev-default.yml +84 -0
  22. package/policy-templates/finance.yml +61 -0
  23. package/policy-templates/strict.yml +49 -0
  24. package/schemas/agent-attestation-v1.json +190 -0
  25. package/schemas/occasio-policy.schema.json +99 -0
  26. package/spec/agent-attestation/v1/README.md +137 -0
  27. package/src/adapters/claude-code.js +518 -0
  28. package/src/adapters/cline.js +161 -0
  29. package/src/adapters/computer-use-cli.js +198 -0
  30. package/src/adapters/computer-use.js +227 -0
  31. package/src/analyzer.js +170 -0
  32. package/src/anomaly/cli.js +143 -0
  33. package/src/anomaly/detectors/deny-rate.js +84 -0
  34. package/src/anomaly/detectors/file-read-volume.js +109 -0
  35. package/src/anomaly/detectors/secret-redact-rate.js +107 -0
  36. package/src/anomaly/detectors/unknown-tool-input.js +83 -0
  37. package/src/anomaly/index.js +169 -0
  38. package/src/attest/canonicalize.js +97 -0
  39. package/src/attest/index.js +355 -0
  40. package/src/attest/run-slice.js +57 -0
  41. package/src/attest/sign.js +186 -0
  42. package/src/attest/verify.js +192 -0
  43. package/src/audit/errors.js +21 -0
  44. package/src/audit/input-normalizer.js +121 -0
  45. package/src/audit/jsonl-auditor.js +178 -0
  46. package/src/audit/verifier.js +152 -0
  47. package/src/baseline.js +507 -0
  48. package/src/boundary.js +238 -0
  49. package/src/budget.js +42 -0
  50. package/src/classifier.js +115 -0
  51. package/src/context-budget.js +77 -0
  52. package/src/core/boundary-event.js +75 -0
  53. package/src/core/decision.js +61 -0
  54. package/src/core/pipeline.js +66 -0
  55. package/src/core/tool-names.js +105 -0
  56. package/src/dashboard.js +892 -0
  57. package/src/demo/README.md +31 -0
  58. package/src/demo/anomalies-demo.js +211 -0
  59. package/src/demo/attest-demo.js +198 -0
  60. package/src/distiller.js +155 -0
  61. package/src/embeddings.json +72 -0
  62. package/src/executor/dispatcher.js +230 -0
  63. package/src/harness.js +817 -0
  64. package/src/index.js +1711 -0
  65. package/src/inspect.js +329 -0
  66. package/src/interceptor.js +1198 -0
  67. package/src/lao.js +185 -0
  68. package/src/lao_prep.py +119 -0
  69. package/src/ledger.js +209 -0
  70. package/src/mcp-experiment.js +140 -0
  71. package/src/mcp-normalize.js +139 -0
  72. package/src/mcp-server.js +320 -0
  73. package/src/outbound-policy.js +433 -0
  74. package/src/policy/built-in-classifiers.js +78 -0
  75. package/src/policy/doctor.js +226 -0
  76. package/src/policy/engine.js +339 -0
  77. package/src/policy/init.js +153 -0
  78. package/src/policy/loader.js +448 -0
  79. package/src/policy/rules-default.js +36 -0
  80. package/src/policy/shell-path.js +135 -0
  81. package/src/policy/show.js +196 -0
  82. package/src/policy/validate.js +310 -0
  83. package/src/preflight/cli.js +164 -0
  84. package/src/preflight/miner.js +329 -0
  85. package/src/proxy/agent-router.js +93 -0
  86. package/src/redteam.js +428 -0
  87. package/src/replay.js +446 -0
  88. package/src/report/index.js +224 -0
  89. package/src/runtime.js +595 -0
  90. package/src/scanner/index.js +49 -0
  91. package/src/selftest.js +192 -0
  92. package/src/session.js +36 -0
@@ -0,0 +1,84 @@
1
+ # yaml-language-server: $schema=https://occasio.ai/schemas/occasio-policy.schema.json
2
+ # Occasio policy — dev-default template (generated by: occasio policy init)
3
+ #
4
+ # Default posture: governance on, blocking on, no path restrictions.
5
+ # All flags are at their built-in defaults; the tools: block is shown
6
+ # commented for reference. Edit this file to customise. Changes take
7
+ # effect immediately on the next tool call (no proxy restart).
8
+ #
9
+ # occasio policy show — view the active policy with annotations
10
+ # occasio policy validate — check this file for errors before using it
11
+ # occasio policy init --template strict — switch to a locked-down preset
12
+ # occasio policy init --template finance — switch to the finance-oriented preset
13
+ version: 1
14
+
15
+ # ── Global flags ──────────────────────────────────────────────────────────────
16
+
17
+ # Block the request when a tool result contains a recognised secret pattern.
18
+ # The model never sees the secret; a policy-refusal response is returned instead.
19
+ block_secrets_in_tool_results: true
20
+
21
+ # Redact secrets in-place (replace with [REDACTED]) rather than blocking outright.
22
+ # To use: set this to true and set block_secrets_in_tool_results to false.
23
+ redact_secrets_in_tool_results: false
24
+
25
+ # Distil long tool outputs (file reads, grep results) before sending to the
26
+ # model. Reduces token cost; the full raw output is still saved locally.
27
+ distill_tool_results: false
28
+
29
+ # Block outbound requests once the session spend reaches the --budget limit.
30
+ block_requests_over_budget: true
31
+
32
+ # ── Per-tool routing (optional) ───────────────────────────────────────────────
33
+ # Uncomment the tools: block below to override routing for specific tools.
34
+ #
35
+ # IMPORTANT: when tools: is present it replaces the built-in defaults entirely.
36
+ # Any tool not listed here will PASS to the cloud. List every tool you want
37
+ # to keep running locally.
38
+ #
39
+ # Available actions:
40
+ # LOCAL — execute locally, result never leaves your machine
41
+ # TRANSFORM — execute locally, then apply a shaping step before sending
42
+ # PASS — skip local execution, let the cloud handle it
43
+ #
44
+ # Available transforms: redact-secrets, distill-output
45
+ # Chaining: set both redact_secrets_in_tool_results and distill_tool_results
46
+ # to true to apply redact → distil automatically on all LOCAL tools.
47
+ #
48
+ # tools:
49
+ #
50
+ # # Distil grep output to send only matched lines, not the full corpus:
51
+ # grep:
52
+ # action: TRANSFORM
53
+ # transform: distill-output
54
+ #
55
+ # # Redact secrets from file reads (replaces recognised patterns):
56
+ # read_file:
57
+ # action: TRANSFORM
58
+ # transform: redact-secrets
59
+ #
60
+ # # Always route shell commands locally (default behaviour, shown for reference):
61
+ # shell_bash:
62
+ # action: LOCAL
63
+ # classifier: bash-allowlist
64
+
65
+ # ── Path-based access control (optional) ──────────────────────────────────────
66
+ # Block or allow filesystem paths regardless of tool routing. Applies to
67
+ # read_file, find_files, and grep. Path entries may use ~ for the user home
68
+ # directory; comparisons are case-insensitive on Windows.
69
+ #
70
+ # deny_paths:
71
+ # - ~/.ssh
72
+ # - ~/.aws
73
+ # - ~/.config/gcloud
74
+ #
75
+ # allow_paths:
76
+ # - ~/projects
77
+
78
+ # ── Custom deny patterns (optional) ───────────────────────────────────────────
79
+ # Extend the built-in secret scanner with your own regexes. Patterns are
80
+ # JavaScript RegExps; escape backslashes for YAML.
81
+ #
82
+ # deny_patterns:
83
+ # internal-jwt: "eyJ[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+"
84
+ # internal-ticket: "INC-[0-9]{6,}"
@@ -0,0 +1,61 @@
1
+ # yaml-language-server: $schema=https://occasio.ai/schemas/occasio-policy.schema.json
2
+ # Occasio policy — finance template
3
+ #
4
+ # Posture for finance / banking environments where the regulated data
5
+ # surface includes account numbers, SWIFT/BIC identifiers, and locale-
6
+ # specific PII patterns the built-in scanner does not cover.
7
+ #
8
+ # This template ADDS deny_patterns on top of the dev-default posture; it
9
+ # does not lock paths down. Pair with a strict allow_paths list if the
10
+ # deployment also requires project-scoped access.
11
+ #
12
+ # Compliance reference: docs/compliance-mapping.md (DRAFT) maps the
13
+ # stanzas in this file to SOC 2 CC controls.
14
+ version: 1
15
+
16
+ # ── Global flags ──────────────────────────────────────────────────────────────
17
+ block_secrets_in_tool_results: true
18
+ redact_secrets_in_tool_results: false
19
+ distill_tool_results: true
20
+ block_requests_over_budget: true
21
+
22
+ # ── Path-based access control ─────────────────────────────────────────────────
23
+ # Deny common credential locations. Add organisation-specific paths
24
+ # (key vaults, signed-config stores) as needed.
25
+ deny_paths:
26
+ - ~/.ssh
27
+ - ~/.aws
28
+ - ~/.config/gcloud
29
+ - ~/.gnupg
30
+
31
+ # allow_paths intentionally left empty (deny_paths is still enforced).
32
+ # Set this to your repo roots if you want to restrict reads further.
33
+ # allow_paths:
34
+ # - ~/projects
35
+
36
+ # ── Custom deny patterns ──────────────────────────────────────────────────────
37
+ # Each pattern below is a JavaScript regex. Tested against tool output;
38
+ # a match triggers BLOCK (or REDACT if redact_secrets_in_tool_results is on).
39
+ deny_patterns:
40
+ # SWIFT / BIC code (8 or 11 chars, ISO 9362). Covers most outbound
41
+ # international wire-transfer artefacts that might appear in logs or
42
+ # generated documents.
43
+ swift-bic: "\\b[A-Z]{4}[A-Z]{2}[A-Z0-9]{2}([A-Z0-9]{3})?\\b"
44
+
45
+ # IBAN (International Bank Account Number, ISO 13616). Length varies by
46
+ # country (15–34 chars); this regex captures the common shape conservatively.
47
+ iban: "\\b[A-Z]{2}[0-9]{2}[A-Z0-9]{11,30}\\b"
48
+
49
+ # US Social Security Number (xxx-xx-xxxx). Conservative — matches the
50
+ # canonical hyphenated form only, to keep false positives down.
51
+ ssn-us: "\\b[0-9]{3}-[0-9]{2}-[0-9]{4}\\b"
52
+
53
+ # Internal change / incident ticket IDs (typical Jira/ServiceNow shapes).
54
+ # Customise per-org; ticket IDs in tool output are often associated
55
+ # with sensitive change context.
56
+ internal-ticket: "(INC|CHG|RITM)-[0-9]{6,}"
57
+
58
+ # Internal JWT format (Anthropic / Auth0 / generic three-segment token).
59
+ # The built-in scanner already catches many JWTs; this is an extra
60
+ # belt-and-braces match for organisation-specific tokens.
61
+ jwt: "eyJ[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+"
@@ -0,0 +1,49 @@
1
+ # yaml-language-server: $schema=https://occasio.ai/schemas/occasio-policy.schema.json
2
+ # Occasio policy — strict template
3
+ #
4
+ # Locked-down posture for deployments where the agent should only ever
5
+ # touch a small, declared set of project paths and never see credentials
6
+ # of any kind.
7
+ #
8
+ # Before activating: review the allow_paths list — the default below
9
+ # allows ONLY ~/projects. Anything outside that prefix is blocked, even
10
+ # read_file calls. On Windows, paths are matched case-insensitively;
11
+ # both POSIX (~/.ssh) and Windows-style (C:\Users\…\.ssh) deny entries
12
+ # are honoured.
13
+ #
14
+ # occasio policy show — confirm the active policy
15
+ # occasio policy validate — lint this file
16
+ version: 1
17
+
18
+ # ── Global flags ──────────────────────────────────────────────────────────────
19
+ block_secrets_in_tool_results: true
20
+ redact_secrets_in_tool_results: false
21
+ distill_tool_results: true
22
+ block_requests_over_budget: true
23
+
24
+ # ── Path-based access control ─────────────────────────────────────────────────
25
+ # Deny common credential / config locations. The list intentionally covers
26
+ # the same prefixes on both Linux/macOS (~) and Windows (C:\Users\<you>\…),
27
+ # so the same template works across the team without per-platform edits.
28
+ deny_paths:
29
+ - ~/.ssh
30
+ - ~/.aws
31
+ - ~/.config/gcloud
32
+ - ~/.gnupg
33
+ - ~/.kube
34
+ - ~/.docker
35
+ - ~/.netrc
36
+
37
+ # Allow ONLY this prefix. Anything outside is blocked. Widen this for your
38
+ # project layout — for example, add ~/work, ~/code, or specific repos.
39
+ # An empty allow_paths list disables the allowlist (deny_paths still applies).
40
+ allow_paths:
41
+ - ~/projects
42
+
43
+ # ── Custom deny patterns ──────────────────────────────────────────────────────
44
+ # Conservative additions to the built-in scanner. The patterns below cover
45
+ # common internal token / ticket shapes that a generic secret scanner would
46
+ # miss. Extend per-org as needed.
47
+ deny_patterns:
48
+ internal-jwt: "eyJ[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+"
49
+ internal-ticket: "INC-[0-9]{6,}"
@@ -0,0 +1,190 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://github.com/occasiolabs/occasio/spec/agent-attestation/v1",
4
+ "title": "Occasio Agent Behavioral Attestation v1",
5
+ "description": "Self-contained, schema-validated commitment to a slice of a Occasio tamper-evident audit chain. Summarises what an AI coding agent did during one governed session (run_id) — every tool call's decision, blocked attempts, redacted secrets, the active policy's digest, and the first/last hashes of the chain segment that backs the claim. Intended to be later wrapped in an in-toto Statement envelope and signed via Sigstore keyless. v1 carries an unsigned variant (signature: null); subsequent versions populate the signature object.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": [
9
+ "schema_version",
10
+ "predicate_type",
11
+ "subject",
12
+ "agent",
13
+ "policy",
14
+ "execution_summary",
15
+ "audit_chain",
16
+ "signature"
17
+ ],
18
+ "properties": {
19
+ "schema_version": {
20
+ "type": "string",
21
+ "const": "1.0.0"
22
+ },
23
+ "predicate_type": {
24
+ "type": "string",
25
+ "const": "https://github.com/occasiolabs/occasio/spec/agent-attestation/v1"
26
+ },
27
+ "subject": {
28
+ "type": "object",
29
+ "additionalProperties": false,
30
+ "required": ["run_id"],
31
+ "properties": {
32
+ "run_id": {
33
+ "type": "string",
34
+ "description": "UUID of the Occasio proxy run that produced the attested events."
35
+ },
36
+ "git_commit": {
37
+ "type": ["string", "null"],
38
+ "description": "Reserved for v1.1 — populated when the attestation is bound to a single git commit (set by GitHub Action in Phase 2)."
39
+ },
40
+ "files_changed": {
41
+ "type": "array",
42
+ "items": { "type": "string" },
43
+ "description": "Reserved for v1.1 — paths modified by the agent during this run."
44
+ }
45
+ }
46
+ },
47
+ "agent": {
48
+ "type": "object",
49
+ "additionalProperties": false,
50
+ "required": ["platform", "model", "session_id", "started_at", "ended_at", "wall_time_s"],
51
+ "properties": {
52
+ "platform": {
53
+ "type": "string",
54
+ "description": "Adapter that produced these events (e.g. 'claude-code', 'cline', 'mcp')."
55
+ },
56
+ "model": {
57
+ "type": ["string", "null"],
58
+ "description": "Model ID derived from request rows in the chain slice, if available."
59
+ },
60
+ "session_id": {
61
+ "type": ["string", "null"]
62
+ },
63
+ "started_at": {
64
+ "type": "string",
65
+ "format": "date-time"
66
+ },
67
+ "ended_at": {
68
+ "type": "string",
69
+ "format": "date-time"
70
+ },
71
+ "wall_time_s": {
72
+ "type": "integer",
73
+ "minimum": 0
74
+ }
75
+ }
76
+ },
77
+ "policy": {
78
+ "type": "object",
79
+ "additionalProperties": false,
80
+ "required": ["file_hash", "file_path", "source", "version", "rules_digest"],
81
+ "properties": {
82
+ "file_hash": {
83
+ "type": "string",
84
+ "pattern": "^[0-9a-f]{64}$",
85
+ "description": "SHA-256 of the active policy.yml file bytes (sentinel 0*64 if no file was present)."
86
+ },
87
+ "file_path": {
88
+ "type": ["string", "null"]
89
+ },
90
+ "source": {
91
+ "type": "string",
92
+ "enum": ["user", "default", "unknown", "inferred"],
93
+ "description": "How the file_hash was determined. 'user'/'default'/'unknown' all reflect a policy_loaded event observed inside the run slice. 'inferred' means no such event was found and file_hash was computed from the policy file's current on-disk bytes at attest time — which may differ from what was active during the run. Verifiers SHOULD treat 'inferred' as weaker evidence."
94
+ },
95
+ "version": {
96
+ "type": ["integer", "null"]
97
+ },
98
+ "rules_digest": {
99
+ "type": "object",
100
+ "additionalProperties": false,
101
+ "properties": {
102
+ "deny_paths_count": { "type": "integer", "minimum": 0 },
103
+ "deny_patterns_count": { "type": "integer", "minimum": 0 },
104
+ "block_secrets": { "type": "boolean" }
105
+ }
106
+ }
107
+ }
108
+ },
109
+ "execution_summary": {
110
+ "type": "object",
111
+ "additionalProperties": false,
112
+ "required": ["tool_calls", "local", "passed", "blocked", "transformed", "secrets_redacted", "blocked_events"],
113
+ "properties": {
114
+ "tool_calls": { "type": "integer", "minimum": 0 },
115
+ "local": { "type": "integer", "minimum": 0 },
116
+ "passed": { "type": "integer", "minimum": 0 },
117
+ "blocked": { "type": "integer", "minimum": 0 },
118
+ "transformed": { "type": "integer", "minimum": 0 },
119
+ "secrets_redacted":{ "type": "integer", "minimum": 0 },
120
+ "blocked_events": {
121
+ "type": "array",
122
+ "items": {
123
+ "type": "object",
124
+ "additionalProperties": false,
125
+ "required": ["tool", "rule", "at_offset_s"],
126
+ "properties": {
127
+ "tool": { "type": "string" },
128
+ "target": { "type": ["string", "null"] },
129
+ "rule": { "type": "string" },
130
+ "at_offset_s": { "type": "integer", "minimum": 0 }
131
+ }
132
+ }
133
+ }
134
+ }
135
+ },
136
+ "audit_chain": {
137
+ "type": "object",
138
+ "additionalProperties": false,
139
+ "required": ["genesis", "first_hash", "last_hash", "event_count", "chain_file", "verifier_url"],
140
+ "properties": {
141
+ "genesis": {
142
+ "type": "string",
143
+ "const": "0000000000000000000000000000000000000000000000000000000000000000"
144
+ },
145
+ "first_hash": {
146
+ "type": ["string", "null"],
147
+ "description": "Hash field of the first attested row (null when event_count == 0).",
148
+ "pattern": "^[0-9a-f]{64}$"
149
+ },
150
+ "last_hash": {
151
+ "type": ["string", "null"],
152
+ "pattern": "^[0-9a-f]{64}$"
153
+ },
154
+ "event_count": {
155
+ "type": "integer",
156
+ "minimum": 0
157
+ },
158
+ "chain_file": {
159
+ "type": "string"
160
+ },
161
+ "verifier_url": {
162
+ "type": "string",
163
+ "format": "uri"
164
+ }
165
+ }
166
+ },
167
+ "signature": {
168
+ "description": "Sigstore keyless / cosign signature. v1 Phase-0 attestations are unsigned (null); Phase 1 populates the object.",
169
+ "oneOf": [
170
+ { "type": "null" },
171
+ {
172
+ "type": "object",
173
+ "additionalProperties": false,
174
+ "required": ["type", "identity", "signed_at"],
175
+ "properties": {
176
+ "type": { "type": "string", "enum": ["sigstore-cosign-keyless"] },
177
+ "identity": { "type": "string" },
178
+ "rekor_entry": { "type": ["string", "null"], "format": "uri" },
179
+ "signed_at": { "type": "string", "format": "date-time" },
180
+ "envelope_sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" }
181
+ }
182
+ }
183
+ ]
184
+ },
185
+ "generated_at": {
186
+ "type": "string",
187
+ "format": "date-time"
188
+ }
189
+ }
190
+ }
@@ -0,0 +1,99 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://occasio.ai/schemas/occasio-policy.schema.json",
4
+ "title": "Occasio Policy",
5
+ "description": "Schema for ~/.occasio/policy.yml — the single document that governs how Occasio handles tool calls and tool results. Authoritative reference: src/policy/loader.js (parser) and src/policy/validate.js (linter). Stable across v0.6.x.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "properties": {
9
+ "version": {
10
+ "type": "integer",
11
+ "description": "Policy schema version. Currently 1.",
12
+ "const": 1
13
+ },
14
+ "block_secrets_in_tool_results": {
15
+ "type": "boolean",
16
+ "description": "Block the request when a tool result contains a recognised secret pattern. The model never sees the secret; a synthetic refusal is returned instead."
17
+ },
18
+ "redact_secrets_in_tool_results": {
19
+ "type": "boolean",
20
+ "description": "Replace recognised secrets with [REDACTED] in-place rather than blocking outright. Mutually intended with block_secrets_in_tool_results=false."
21
+ },
22
+ "distill_tool_results": {
23
+ "type": "boolean",
24
+ "description": "Distil long tool outputs (file reads, grep results) before sending to the model. Reduces token cost; the full raw output is still saved locally."
25
+ },
26
+ "block_requests_over_budget": {
27
+ "type": "boolean",
28
+ "description": "Block outbound requests once the session spend reaches the --budget limit. Returns HTTP 402 without invoking the model."
29
+ },
30
+ "tools": {
31
+ "type": "object",
32
+ "description": "Per-tool routing. When present, REPLACES the built-in defaults entirely — every tool not listed here will PASS to the cloud. Use sparingly; the defaults are usually what you want.",
33
+ "additionalProperties": {
34
+ "$ref": "#/$defs/toolEntry"
35
+ }
36
+ },
37
+ "deny_paths": {
38
+ "type": "array",
39
+ "description": "Filesystem prefixes denied for read_file, find_files, and grep. Comparisons are on the symlink-resolved absolute path; case-insensitive on Windows. ~ is expanded to the user's home directory.",
40
+ "items": { "type": "string", "minLength": 1 },
41
+ "uniqueItems": true
42
+ },
43
+ "allow_paths": {
44
+ "type": "array",
45
+ "description": "Filesystem prefixes allowed for read_file, find_files, and grep. When non-empty, access is restricted to these prefixes; anything outside is blocked. Empty (the default) means no allowlist.",
46
+ "items": { "type": "string", "minLength": 1 },
47
+ "uniqueItems": true
48
+ },
49
+ "deny_patterns": {
50
+ "type": "object",
51
+ "description": "Custom regex patterns that, when matched in a tool result, are treated like secret-scanner hits — blocked or redacted depending on the global flags. Each entry is label: \"regex-string\".",
52
+ "additionalProperties": {
53
+ "type": "string",
54
+ "minLength": 1,
55
+ "description": "JavaScript-flavored regular expression source (without leading/trailing slashes)."
56
+ }
57
+ }
58
+ },
59
+ "$defs": {
60
+ "toolEntry": {
61
+ "type": "object",
62
+ "additionalProperties": false,
63
+ "required": ["action"],
64
+ "properties": {
65
+ "action": {
66
+ "type": "string",
67
+ "enum": ["PASS", "LOCAL", "TRANSFORM"],
68
+ "description": "What the dispatcher does with this tool call. PASS forwards to the cloud; LOCAL executes on the user's machine; TRANSFORM executes locally then applies a shaping step."
69
+ },
70
+ "transform": {
71
+ "type": "string",
72
+ "description": "Required when action is TRANSFORM. Built-in values: redact-secrets, distill-output. Other names are forward-compatible but only execute if the dispatcher knows about them.",
73
+ "minLength": 1
74
+ },
75
+ "executor": {
76
+ "type": "string",
77
+ "description": "Optional executor name. Default: native.",
78
+ "minLength": 1
79
+ },
80
+ "classifier": {
81
+ "type": "string",
82
+ "description": "Optional input-validation classifier. Built-in values: read-input-validator, glob-input-validator, grep-input-validator, todo-write-validator, todo-read-validator, bash-allowlist, powershell-allowlist.",
83
+ "minLength": 1
84
+ },
85
+ "reason": {
86
+ "type": "string",
87
+ "description": "Optional reason string surfaced in audit rows when this entry's action fires.",
88
+ "minLength": 1
89
+ }
90
+ },
91
+ "allOf": [
92
+ {
93
+ "if": { "properties": { "action": { "const": "TRANSFORM" } }, "required": ["action"] },
94
+ "then": { "required": ["transform"] }
95
+ }
96
+ ]
97
+ }
98
+ }
99
+ }
@@ -0,0 +1,137 @@
1
+ # AI-Agent Behavioral Attestation v1
2
+
3
+ **Predicate type URI:** `https://github.com/occasiolabs/occasio/spec/agent-attestation/v1`
4
+
5
+ **Status:** Draft 1 (v1.0.0). Tracked at [`schemas/agent-attestation-v1.json`](../../../schemas/agent-attestation-v1.json).
6
+
7
+ **License:** Apache-2.0.
8
+
9
+ > **Note on URI resolvability.** The predicate type URI above is currently a stable identifier rather than a fetchable resource — a `GET` against it will not return this spec document. (Standard practice for predicate URIs varies: SLSA Provenance and CycloneDX URIs resolve to content today; this one will resolve once a static-site mirror is published at the canonical URL. The identifier is reserved and will not move.) Consumers must compare the URI as a string, not dereference it.
10
+
11
+ ## What this predicate claims
12
+
13
+ A signed cryptographic statement about **what an AI coding agent did during a single bounded session**, including every governed tool call, every blocked attempt, every transform applied to a tool result, and the active policy that produced those decisions — all bound to a tamper-evident audit chain.
14
+
15
+ The predicate answers the question **"What did the AI agent actually do while producing this artifact?"** — which is orthogonal to, and complementary with:
16
+
17
+ - **SLSA Provenance** (what build process produced the artifact)
18
+ - **CycloneDX ML-BOM / AIBOM** (what model + training data sit in the AI system)
19
+
20
+ None of those predicate types describe **runtime behavioral integrity**: which files were read, which were blocked, which secrets were redacted, which policy was enforcing the call. This predicate fills that gap.
21
+
22
+ ## Use cases
23
+
24
+ - A pull request opened by an AI coding agent ships an attestation as a GitHub Check. A reviewer sees `47 tool calls · 2 blocked · 1 secret redacted` before merge.
25
+ - A SOC2 auditor receives signed attestations covering every AI-assisted change in the audit period and re-walks the chain offline using an independent verifier.
26
+ - An enterprise mandates that every vendor's AI-agent contribution carries a behavioral attestation conformant to this predicate type.
27
+
28
+ ## Envelope
29
+
30
+ This predicate is intended to be wrapped in an [in-toto Statement v1](https://github.com/in-toto/attestation/blob/main/spec/v1/statement.md) and signed via [Sigstore](https://www.sigstore.dev/) (keyless, GitHub OIDC). The Statement carries:
31
+
32
+ ```jsonc
33
+ {
34
+ "_type": "https://in-toto.io/Statement/v1",
35
+ "predicateType": "https://github.com/occasiolabs/occasio/spec/agent-attestation/v1",
36
+ "subject": [{
37
+ "name": "occasio:run:<uuid>",
38
+ "digest": { "sha256": "<last_hash from the run's audit chain slice>" }
39
+ }],
40
+ "predicate": { /* the v1 object below */ }
41
+ }
42
+ ```
43
+
44
+ The `subject.digest.sha256` is the `last_hash` of the audit-chain slice attested by this predicate. Because the chain hash-links every event back to the GENESIS sentinel via `prev_hash → hash`, signing this single hash transitively commits to every event in the slice.
45
+
46
+ ## Predicate fields (v1.0.0)
47
+
48
+ | Field | Type | Required | Notes |
49
+ |---|---|---|---|
50
+ | `schema_version` | string | yes | `"1.0.0"` |
51
+ | `predicate_type` | string | yes | Constant: predicate URI above |
52
+ | `subject.run_id` | string | yes | UUID identifying the agent session |
53
+ | `subject.git_commit` | string \| null | no | Set in CI when the attestation is bound to a commit |
54
+ | `subject.files_changed` | string[] | no | Paths modified during the run |
55
+ | `agent.platform` | string | yes | e.g. `claude-code`, `cline`, `mcp` |
56
+ | `agent.model` | string \| null | yes | Model ID, if surfaced by the adapter |
57
+ | `agent.session_id` | string \| null | yes | Adapter-level session ID |
58
+ | `agent.started_at` | RFC 3339 | yes | First event in slice |
59
+ | `agent.ended_at` | RFC 3339 | yes | Last event in slice |
60
+ | `agent.wall_time_s` | integer | yes | Elapsed seconds |
61
+ | `policy.file_hash` | hex-64 | yes | SHA-256 of the active policy.yml bytes (or `0`*64 when no file) |
62
+ | `policy.file_path` | string \| null | yes | Path on the producer's machine |
63
+ | `policy.source` | enum | yes | `user`, `default`, `unknown`, or `inferred` (see below) |
64
+ | `policy.version` | integer \| null | yes | Policy file `version:` field |
65
+ | `policy.rules_digest` | object | yes | `deny_paths_count`, `deny_patterns_count`, `block_secrets` |
66
+ | `execution_summary.tool_calls` | integer | yes | Total in slice (excluding policy events) |
67
+ | `execution_summary.local` | integer | yes | `LOCAL` decisions |
68
+ | `execution_summary.passed` | integer | yes | `PASS` decisions |
69
+ | `execution_summary.blocked` | integer | yes | `BLOCK` decisions |
70
+ | `execution_summary.transformed` | integer | yes | `TRANSFORM` decisions |
71
+ | `execution_summary.secrets_redacted` | integer | yes | Σ secrets redacted across slice |
72
+ | `execution_summary.blocked_events` | array | no | One entry per BLOCK with `tool`, `target`, `rule`, `at_offset_s` |
73
+ | `audit_chain.genesis` | hex-64 | yes | Constant: 64 zeros |
74
+ | `audit_chain.first_hash` | hex-64 \| null | yes | Hash of first row in slice |
75
+ | `audit_chain.last_hash` | hex-64 \| null | yes | Hash of last row in slice |
76
+ | `audit_chain.event_count` | integer | yes | Number of rows in slice |
77
+ | `audit_chain.chain_file` | string | yes | Path to the chain file (advisory) |
78
+ | `audit_chain.verifier_url` | URI | yes | Where to fetch the independent verifier |
79
+ | `signature` | object \| null | yes | `null` for unsigned; populated when Sigstore-signed |
80
+ | `generated_at` | RFC 3339 | no | Producer timestamp |
81
+
82
+ The full JSON Schema (Draft-07) is the authoritative source: [`schemas/agent-attestation-v1.json`](../../../schemas/agent-attestation-v1.json).
83
+
84
+ ### `policy.source` values
85
+
86
+ - `user` — a `policy_loaded` event was observed inside the run slice and the active policy came from a user-supplied file.
87
+ - `default` — a `policy_loaded` event was observed and the producer was running on its built-in default policy.
88
+ - `unknown` — a `policy_loaded` event was observed but its `policy_source` field was not set.
89
+ - `inferred` — **no `policy_loaded` event was found in the slice.** The producer fell back to hashing the policy file's bytes *at attest time*, which may have been edited after the run ended. Verifiers SHOULD treat `inferred` as weaker evidence than the other three and surface this distinction to the user.
90
+
91
+ ## Verification model
92
+
93
+ A consumer of an attestation must perform three independent checks, in order, all of which must pass:
94
+
95
+ 1. **Sigstore signature.** Verify the Sigstore Bundle's certificate chain (Fulcio root) and inclusion proof (Rekor transparency log). This proves *who* produced the predicate.
96
+ 2. **Predicate ↔ payload equivalence.** Re-decode the DSSE envelope inside the Sigstore Bundle, parse its payload as an in-toto Statement, and compare its `predicate` (modulo the `signature` metadata field) byte-for-byte with the attestation JSON in hand. This proves *that the predicate file has not been swapped*.
97
+ 3. **Audit chain integrity.** Re-walk the `chain_file` end-to-end using the canonical SHA-256 walker, and confirm that `first_hash` and `last_hash` appear in that chain in the correct relative order. This proves *the predicate reflects events that actually happened*.
98
+
99
+ Each check is a hard requirement. A consumer that skips any of them is not verifying this predicate.
100
+
101
+ Reference verifiers are available in two languages:
102
+ - **Node:** `occasio attest verify` (all three steps in one call).
103
+ - **Python:** [`docs/attest_verify.py`](../../../docs/attest_verify.py) (stdlib + optional `sigstore-python`; reuses [`docs/audit_walker.py`](../../../docs/audit_walker.py) for the chain step). See [`docs/python-verifier.md`](../../../docs/python-verifier.md).
104
+
105
+ **Cross-language invariant.** Both verifiers produce byte-identical canonical forms of the predicate and identical pass/fail decisions for the audit-chain step on the same payload, including for predicate-tampered and chain-tampered inputs. Both canonicalize implementations explicitly reject non-integer numbers so a future schema addition cannot silently introduce divergence. The test suite asserts this under `xlang:` and `xlang-float:` cases.
106
+
107
+ **Sigstore signature step.** Uses the standard DSSE-wrapped in-toto Statement format. Independently verifiable by any [sigstore-conformant](https://www.sigstore.dev/) tool — `cosign verify-blob`, `sigstore-js`, or `sigstore-python`. The reference test suite mocks the signing path for determinism; a real-OIDC signed-and-verified round-trip requires a GitHub Actions environment with `permissions: id-token: write` and is exercised by the reference Action when used in CI.
108
+
109
+ ## Compatibility and stability
110
+
111
+ - **Breaking changes** to required fields require a new predicate URI (`/v2`, etc.).
112
+ - **Additive changes** (new optional fields) are permitted in `v1.x` and do not bump the URI.
113
+ - The predicate URI in this spec is **canonical and stable**. It will not be moved or re-pointed.
114
+ - Consumers MUST refuse to verify attestations whose `predicate_type` does not match exactly.
115
+
116
+ ## Relationship to other specifications
117
+
118
+ | Spec | Relationship |
119
+ |---|---|
120
+ | [in-toto Attestation Framework](https://github.com/in-toto/attestation) | Carrier envelope. This predicate type is a v1 in-toto Statement payload. |
121
+ | [SLSA Provenance](https://slsa.dev/spec/v1.0/provenance) | Complementary. SLSA describes builds; this predicate describes AI-agent runtime behavior. |
122
+ | [CycloneDX ML-BOM / OWASP AIBOM](https://cyclonedx.org/capabilities/mlbom/) | Complementary. ML-BOM describes models/data; this predicate describes session-level agent actions. |
123
+ | [Sigstore](https://www.sigstore.dev/) | Signing transport. Reference implementation uses Sigstore keyless via GitHub OIDC. |
124
+
125
+ ## Open questions (tracked for v1.1 / v2)
126
+
127
+ - **Multi-commit attestations.** Today the attestation binds to a single `run_id`. A natural extension: one attestation per PR covering N commits from M runs. v1.1 will likely add `subject.git_commits[]` and a way to merge slices.
128
+ - **Policy provenance.** Today `policy.file_hash` commits to the file bytes. We do not carry the *origin* of the policy (was it committed to a repo? signed by a security team?). v1.1 may add `policy.attestation_url` for nested signed claims.
129
+ - **Event-level disclosure.** Today the chain-file path is advisory; the chain itself is not bundled. A v1.x option may add an embedded compressed chain slice for fully offline verification, at a size cost.
130
+
131
+ ## Reference implementation
132
+
133
+ The producing CLI, full verifier, JSON Schema, and an independent Python audit-chain walker live in [`occasiolabs/occasio`](https://github.com/occasiolabs/occasio). The reference Sigstore-signed attestations from that repo's own CI runs serve as conformance examples (Phase 3 of the rollout plan).
134
+
135
+ ## Authors and contributions
136
+
137
+ Apache-2.0 licensed. Issues and pull requests at [`occasiolabs/occasio`](https://github.com/occasiolabs/occasio). Predicate-type submissions to the [in-toto attestation registry](https://github.com/in-toto/attestation) tracking once production usage and external adopters reach the registry's bar.