@fourier-labs/harbour-contracts 0.68.0 → 0.70.0

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.
@@ -2,7 +2,7 @@
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
3
  "$id": "https://schemas.harbour.dev/integration-execute-request.schema.json",
4
4
  "title": "IntegrationExecuteRequest",
5
- "description": "Body of POST /_harbour/integrations/execute (app SDK), forwarded unchanged to the governance development and runtime execute routes. Exactly one declared operation against one declared resource. The identity mode is fixed by the operation for every operation but slack.message.post, and always enforced from an approved grant; any identity/userId/scope field is rejected by additionalProperties. A slack.message.post runs as the app (the IT-approved per-app name from the grant, plus a \"Posted by <app> on Isomorph\" footer) or as the signed-in person (no name, no footer) — the same input either way — and since 0.55.0 the call names which of the app's approved modes it runs under (`mode`): required when IT approved both modes for the app (MODE_REQUIRED otherwise), optional when it approved one (that one is used), and a named mode without an approved grant is refused (MODE_NOT_GRANTED) rather than served under the other, so approval order never decides what a post does. Per-operation input bounds: slack.channel.history {limit? 1..15, default 15}; slack.message.post {text 1..4000 chars} and a required idempotencyKey retained across retries of one explicit Send; gmail.thread.list {limit? 1..15, default 15}; gmail.message.read {messageId}; gmail.message.send {to 1..50 addresses (to+cc+bcc <= 50 in total), cc?, bcc?, subject 1..256 chars, text 1..4000 chars, replyToMessageId?} and a required idempotencyKey like slack.message.post (plain text only, no attachments); warehouse.view.read {columns 1..32, limit? 1..1000, default 100}. Each operation's then-block is the one statement of its input rules: propertyNames.enum names the fields it accepts, required the ones it needs, and per-field overrides (maximum, default) narrow the shared property. Codegen derives IntegrationExecuteInputRules and validateIntegrationExecuteInput (gen-ts/integration-execute-input-rules.ts, gen-go/integration_execute_input_rules.go) from exactly these blocks, so the governance execute route, the app SDK and the kit's local check refuse the same request with the same message. Transport limits (not expressible here): request <= 16 KiB, total deadline 10 s.",
5
+ "description": "Body of POST /_harbour/integrations/execute (app SDK), forwarded unchanged to the governance development and runtime execute routes. Exactly one declared operation against one declared resource. The identity mode is fixed by the operation for every operation but slack.message.post, and always enforced from an approved grant; any identity/userId/scope field is rejected by additionalProperties. A slack.message.post runs as the app (the IT-approved per-app name from the grant, plus a \"Posted by <app> on Isomorph\" footer) or as the signed-in person (no name, no footer) — the same input either way — and since 0.55.0 the call names which of the app's approved modes it runs under (`mode`): required when IT approved both modes for the app (MODE_REQUIRED otherwise), optional when it approved one (that one is used), and a named mode without an approved grant is refused (MODE_NOT_GRANTED) rather than served under the other, so approval order never decides what a post does. Per-operation input bounds: slack.channel.history {limit? 1..15, default 15}; slack.message.post {text 1..4000 chars} and a required idempotencyKey retained across retries of one explicit Send; gmail.thread.list {limit? 1..15, default 15}; gmail.message.read {messageId}; gmail.message.send {to 1..50 addresses (to+cc+bcc <= 50 in total), cc?, bcc?, subject 1..256 chars, text 1..4000 chars, replyToMessageId?} and a required idempotencyKey like slack.message.post (plain text only, no attachments); warehouse.view.read {columns? 1..32, where? 1..16 filters, groupBy? 1..8 columns, aggregates? 1..16, orderBy? 1..8, limit? 1..1000, default 100} (0.70.0: a plain read names columns, an aggregate read names aggregates and optionally groupBy; the cross-field rules — exactly one of columns or aggregates, groupBy needs aggregates, a filter value that fits its op, orderBy naming a returned column, every named column approved — are enforced by governance, not expressible here; ADR 0017). Each operation's then-block is the one statement of its input rules: propertyNames.enum names the fields it accepts, required the ones it needs, and per-field overrides (maximum, default) narrow the shared property. Codegen derives IntegrationExecuteInputRules and validateIntegrationExecuteInput (gen-ts/integration-execute-input-rules.ts, gen-go/integration_execute_input_rules.go) from exactly these blocks, so the governance execute route, the app SDK and the kit's local check refuse the same request with the same message. Transport limits (not expressible here): request <= 16 KiB, total deadline 10 s.",
6
6
  "type": "object",
7
7
  "additionalProperties": false,
8
8
  "required": ["connection", "operation", "resource", "input"],
@@ -28,7 +28,35 @@
28
28
  "maxItems": 32,
29
29
  "items": { "$ref": "app-integrations-declaration.schema.json#/definitions/ColumnName" },
30
30
  "not": { "allOf": [{ "contains": { "const": "*" } }, { "minItems": 2 }] },
31
- "description": "Explicit columns, or `*` alone when the app's approved resource grant allows all current and future columns."
31
+ "description": "warehouse.view.read, a plain read: explicit columns, or `*` alone when the app's approved resource grant allows all current and future columns. An aggregate read names aggregates instead."
32
+ },
33
+ "where": {
34
+ "type": "array",
35
+ "minItems": 1,
36
+ "maxItems": 16,
37
+ "items": { "$ref": "#/definitions/WarehouseFilter" },
38
+ "description": "warehouse.view.read (0.70.0): filters the warehouse applies before it returns, grouping or limits rows. All of them must hold (AND); there is no OR and no LIKE."
39
+ },
40
+ "groupBy": {
41
+ "type": "array",
42
+ "minItems": 1,
43
+ "maxItems": 8,
44
+ "items": { "type": "string", "pattern": "^[A-Za-z_][A-Za-z0-9_]{0,62}$" },
45
+ "description": "warehouse.view.read (0.70.0): columns (never `*`) that group the rows; needs aggregates. Each returned row is one group: its groupBy columns, then its aggregate aliases."
46
+ },
47
+ "aggregates": {
48
+ "type": "array",
49
+ "minItems": 1,
50
+ "maxItems": 16,
51
+ "items": { "$ref": "#/definitions/WarehouseAggregate" },
52
+ "description": "warehouse.view.read (0.70.0): what an aggregate read computes, over each group or over every matching row when there is no groupBy."
53
+ },
54
+ "orderBy": {
55
+ "type": "array",
56
+ "minItems": 1,
57
+ "maxItems": 8,
58
+ "items": { "$ref": "#/definitions/WarehouseOrder" },
59
+ "description": "warehouse.view.read (0.70.0): sort keys, first key first. Without orderBy an aggregate read is sorted by its groupBy columns and a plain read is not sorted at all."
32
60
  }
33
61
  }
34
62
  },
@@ -68,7 +96,7 @@
68
96
  "not": { "required": ["idempotencyKey"] },
69
97
  "properties": {
70
98
  "mode": false,
71
- "input": { "propertyNames": { "enum": ["columns", "limit"] }, "required": ["columns"], "properties": { "limit": { "maximum": 1000, "default": 100 } } }
99
+ "input": { "propertyNames": { "enum": ["columns", "where", "groupBy", "aggregates", "orderBy", "limit"] }, "properties": { "limit": { "maximum": 1000, "default": 100 } } }
72
100
  }
73
101
  }
74
102
  },
@@ -105,7 +133,63 @@
105
133
  ],
106
134
  "definitions": {
107
135
  "EmailAddress": { "type": "string", "minLength": 3, "maxLength": 254, "pattern": "^[^\\s@<>,;\"]+@[^\\s@<>,;\"]+\\.[^\\s@<>,;\"]+$", "description": "One bare email address (no display name)." },
108
- "EmailAddressList": { "type": "array", "minItems": 1, "maxItems": 50, "uniqueItems": true, "items": { "$ref": "#/definitions/EmailAddress" } }
136
+ "EmailAddressList": { "type": "array", "minItems": 1, "maxItems": 50, "uniqueItems": true, "items": { "$ref": "#/definitions/EmailAddress" } },
137
+ "WarehouseFilter": {
138
+ "type": "object",
139
+ "additionalProperties": false,
140
+ "required": ["column", "op"],
141
+ "description": "One warehouse.view.read filter (0.70.0): <column> <op> <value>. The value is always bound as a query parameter, never written into the SQL. Which value an op takes is a governance rule: isNull/isNotNull take none; in/notIn take a non-empty list of one scalar type; every other op takes one scalar, and a boolean only with eq/ne.",
142
+ "properties": {
143
+ "column": { "type": "string", "pattern": "^[A-Za-z_][A-Za-z0-9_]{0,62}$", "description": "The filtered column; never `*`." },
144
+ "op": { "$ref": "#/definitions/WarehouseFilterOp" },
145
+ "value": {
146
+ "description": "The value compared with: a string (a date or timestamp is its ISO string), a number, a boolean, or a list of strings or numbers for in/notIn. Never null; use isNull or isNotNull.",
147
+ "oneOf": [
148
+ { "type": "string", "maxLength": 256 },
149
+ { "type": "number" },
150
+ { "type": "boolean" },
151
+ { "type": "array", "minItems": 1, "maxItems": 100, "items": { "oneOf": [{ "type": "string", "maxLength": 256 }, { "type": "number" }] } }
152
+ ]
153
+ }
154
+ }
155
+ },
156
+ "WarehouseFilterOp": {
157
+ "type": "string",
158
+ "enum": ["eq", "ne", "gt", "gte", "lt", "lte", "in", "notIn", "isNull", "isNotNull"],
159
+ "description": "A filter comparison: = <> > >= < <= IN, NOT IN, IS NULL, IS NOT NULL. Word-like so every value is a valid generated constant."
160
+ },
161
+ "WarehouseAggregate": {
162
+ "type": "object",
163
+ "additionalProperties": false,
164
+ "required": ["fn", "column", "as"],
165
+ "description": "One computed column of an aggregate warehouse.view.read (0.70.0): fn(column) AS as. `*` is a column only for count (a governance rule). The value follows the read's cell rules, so a 64-bit or decimal result may be a string.",
166
+ "properties": {
167
+ "fn": { "$ref": "#/definitions/WarehouseAggregateFn" },
168
+ "column": { "$ref": "app-integrations-declaration.schema.json#/definitions/ColumnName" },
169
+ "as": { "type": "string", "pattern": "^[a-z_][a-z0-9_]{0,62}$", "description": "The name the computed value is returned under; unique in the read and never a groupBy column (governance rules)." }
170
+ }
171
+ },
172
+ "WarehouseAggregateFn": {
173
+ "type": "string",
174
+ "enum": ["count", "sum", "avg", "min", "max"],
175
+ "description": "An aggregate function."
176
+ },
177
+ "WarehouseOrder": {
178
+ "type": "object",
179
+ "additionalProperties": false,
180
+ "required": ["column"],
181
+ "description": "One warehouse.view.read sort key (0.70.0). A plain read sorts by a selected column (any column when it selects `*`); an aggregate read sorts by a groupBy column or an aggregate alias (governance rules).",
182
+ "properties": {
183
+ "column": { "type": "string", "pattern": "^[A-Za-z_][A-Za-z0-9_]{0,62}$", "description": "A column or an aggregate alias; never `*`." },
184
+ "direction": { "$ref": "#/definitions/WarehouseOrderDirection" }
185
+ }
186
+ },
187
+ "WarehouseOrderDirection": {
188
+ "type": "string",
189
+ "enum": ["asc", "desc"],
190
+ "default": "asc",
191
+ "description": "Sort direction; asc when absent."
192
+ }
109
193
  },
110
194
  "examples": [
111
195
  { "connection": "company-slack", "operation": "slack.channel.history", "resource": "team-updates", "input": { "limit": 15 } },
@@ -133,6 +217,36 @@
133
217
  "input": { "text": "Lunch is at noon." },
134
218
  "idempotencyKey": "6f1c2c1e-3a1c-4c3e-9f0e-2b6f7d8a9c12",
135
219
  "mode": "user"
220
+ },
221
+ {
222
+ "connection": "sales-warehouse",
223
+ "operation": "warehouse.view.read",
224
+ "resource": "invoices",
225
+ "input": {
226
+ "columns": ["invoice_id", "region_code", "amount_minor"],
227
+ "where": [
228
+ { "column": "status", "op": "eq", "value": "PAID" },
229
+ { "column": "invoice_date", "op": "gte", "value": "2026-02-01" },
230
+ { "column": "currency", "op": "in", "value": ["USD", "EUR"] },
231
+ { "column": "closed_at", "op": "isNull" }
232
+ ],
233
+ "limit": 500
234
+ }
235
+ },
236
+ {
237
+ "connection": "sales-warehouse",
238
+ "operation": "warehouse.view.read",
239
+ "resource": "invoices",
240
+ "input": {
241
+ "where": [{ "column": "status", "op": "eq", "value": "PAID" }],
242
+ "groupBy": ["region_code"],
243
+ "aggregates": [
244
+ { "fn": "sum", "column": "amount_minor", "as": "total_minor" },
245
+ { "fn": "count", "column": "*", "as": "invoices" }
246
+ ],
247
+ "orderBy": [{ "column": "total_minor", "direction": "desc" }],
248
+ "limit": 100
249
+ }
136
250
  }
137
251
  ]
138
252
  }
@@ -2,7 +2,7 @@
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
3
  "$id": "https://schemas.harbour.dev/integration-execute-result.schema.json",
4
4
  "title": "IntegrationExecuteResult",
5
- "description": "The `data` member of a successful execute response `{data, requestId}`; the SDK unwraps `data`. Exactly one of the six operation results: slack.channel.history -> {messages, hasMore} (plain text/IDs/time, plus the display name and emoji icon a per-app-named app post carries — authorName/authorIcon, 0.55.0 — never the provider payload); slack.message.post -> {messageId}; gmail.thread.list -> {threads, hasMore} (newest inbox threads; each `messageId` is the thread's newest message, what gmail.message.read takes); gmail.message.read -> {id, threadId, subject, from, to, date, text, truncated} (headers plus the plain text with html stripped, cut at 16 KiB with `truncated` set); gmail.message.send -> {messageId, threadId} (the Gmail ids of the sent message and its thread); warehouse.view.read -> {rows} (decimal/64-bit values as strings, dates as ISO strings). An unknown send outcome is never a result; it is an error with details.code SEND_OUTCOME_UNKNOWN. Results over 1 MiB are rejected, not truncated.",
5
+ "description": "The `data` member of a successful execute response `{data, requestId}`; the SDK unwraps `data`. Exactly one of the six operation results: slack.channel.history -> {messages, hasMore} (plain text/IDs/time, plus the display name and emoji icon a per-app-named app post carries — authorName/authorIcon, 0.55.0 — never the provider payload); slack.message.post -> {messageId}; gmail.thread.list -> {threads, hasMore} (newest inbox threads; each `messageId` is the thread's newest message, what gmail.message.read takes); gmail.message.read -> {id, threadId, subject, from, to, date, text, truncated} (headers plus the plain text with html stripped, cut at 16 KiB with `truncated` set); gmail.message.send -> {messageId, threadId} (the Gmail ids of the sent message and its thread); warehouse.view.read -> {rows, truncated} (rows keyed by the selected columns, or for an aggregate read by the groupBy columns then the aggregate aliases; decimal/64-bit values as strings, dates as ISO strings; since 0.70.0 every server sends `truncated`, true when more rows matched than `limit` — the server reads limit+1 rows to know — so the rows are incomplete and must not be totalled or averaged; a server before 0.70.0 omitted it). An unknown send outcome is never a result; it is an error with details.code SEND_OUTCOME_UNKNOWN. A whole result over 1 MiB is rejected, never cut down to fit.",
6
6
  "type": "object",
7
7
  "additionalProperties": false,
8
8
  "properties": {
@@ -25,11 +25,11 @@
25
25
  "to": { "type": "string", "maxLength": 8192 },
26
26
  "date": { "type": "string", "maxLength": 128 },
27
27
  "text": { "type": "string", "maxLength": 16384 },
28
- "truncated": { "type": "boolean" },
28
+ "truncated": { "type": "boolean", "description": "gmail.message.read: the text was cut at 16 KiB. warehouse.view.read (0.70.0): more rows matched than the read's limit, so `rows` is incomplete." },
29
29
  "rows": {
30
30
  "type": "array",
31
31
  "maxItems": 1000,
32
- "items": { "type": "object", "additionalProperties": {}, "description": "One row keyed by the requested column names." }
32
+ "items": { "type": "object", "additionalProperties": {}, "description": "One row keyed by the selected column names, or by the groupBy columns then the aggregate aliases." }
33
33
  }
34
34
  },
35
35
  "oneOf": [
@@ -38,7 +38,7 @@
38
38
  { "required": ["messageId", "threadId"], "maxProperties": 2 },
39
39
  { "required": ["threads", "hasMore"], "maxProperties": 2 },
40
40
  { "required": ["id", "threadId", "subject", "from", "to", "date", "text", "truncated"], "maxProperties": 8 },
41
- { "required": ["rows"], "maxProperties": 1 }
41
+ { "required": ["rows"], "propertyNames": { "enum": ["rows", "truncated"] } }
42
42
  ],
43
43
  "definitions": {
44
44
  "GmailThread": {
@@ -80,6 +80,7 @@
80
80
  "hasMore": false
81
81
  },
82
82
  { "id": "18f2c3a4b5d6e7f8", "threadId": "18f2c3a4b5d6e7f0", "subject": "Weekly report", "from": "Jane <jane@example.com>", "to": "team@example.com", "date": "2026-09-11T08:00:00Z", "text": "Shipped the weekly report.", "truncated": false },
83
- { "rows": [{ "week": "2026-W36", "total": "1234.50" }] }
83
+ { "rows": [{ "week": "2026-W36", "total": "1234.50" }] },
84
+ { "rows": [{ "region_code": "EMEA", "total_minor": "1234550", "invoices": "42" }, { "region_code": "APAC", "total_minor": "998000", "invoices": "17" }], "truncated": true }
84
85
  ]
85
86
  }
@@ -2,10 +2,10 @@
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
3
  "$id": "https://schemas.harbour.dev/security-scan-input-v2.schema.json",
4
4
  "title": "SecurityScanInputV2",
5
- "description": "Input to the Security Assurance machine, second shape, replacing security-scan-input.schema.json. V1 scanned a deployed commit located through the deployment state; v2 scans exactly one thing: an uploaded package that governance validated at source intake, identified by the zip sha256 the stager verifies before extraction and by the tree digest governance computed from the unzipped bytes (`sourceDigestOf`, `sha256:`-prefixed), which keys the governance scan record and the check report on the laptop. Commit-mode and ARTIFACT-phase inputs are deliberately absent: the console's later on-demand scan uses this same package path against the deploy upload, and the Transform lane runs the scanner in-process; an image scan, if it returns, is an additive v3. The policy profile is named by the caller when it knows better than the data classification (the kit lane names BASELINE); `profile` and `classification` are mutually exclusive, and both absent still defaults to STANDARD with `classificationDefaulted: true`. Nothing here carries a scanner version: the scanner stamps its own into verdict provenance, and governance decides validity by comparing that against its MINIMUM_SCANNER_VERSION. `org`/`app` are the tenant and app ids for kit apps; `env` stays required because the evidence key carries it (governance passes `preview` for a package scan; promotion never re-scans). The `execution` block names the tenant's execution-account scanner, invoke role and artifacts bucket, resolved by governance the way admission resolves them for the deploy machine.",
5
+ "description": "Input to the Security Assurance machine, second shape, replacing security-scan-input.schema.json. V1 scanned a deployed commit located through the deployment state; v2 scans exactly one thing: an uploaded package that governance validated at source intake, identified by the zip sha256 the stager verifies before extraction and by the tree digest governance computed from the unzipped bytes (`sourceDigestOf`, `sha256:`-prefixed), which keys the governance scan record and the check report on the laptop. Commit-mode and ARTIFACT-phase inputs are deliberately absent: the console's later on-demand scan uses this same package path against the deploy upload, and the Transform lane runs the scanner in-process; an image scan, if it returns, is an additive v3. The caller always names the policy profile (governance names BASELINE for the kit lane); there is no classification input and no default profile. Nothing here carries a scanner version: the scanner stamps its own into verdict provenance, and governance decides validity by comparing that against its MINIMUM_SCANNER_VERSION. `org`/`app` are the tenant and app ids for kit apps; `env` stays required because the evidence key carries it (governance passes `preview` for a package scan; promotion never re-scans). The `execution` block names the tenant's execution-account scanner, invoke role and artifacts bucket, resolved by governance the way admission resolves them for the deploy machine.",
6
6
  "type": "object",
7
7
  "additionalProperties": false,
8
- "required": ["schema", "org", "app", "env", "package", "execution"],
8
+ "required": ["schema", "org", "app", "env", "package", "execution", "profile"],
9
9
  "properties": {
10
10
  "schema": { "type": "string", "const": "harbour.security-scan-input/2.0" },
11
11
  "org": { "type": "string", "minLength": 1, "description": "Isomorph organization id (the governance tenant id for kit apps), e.g. fourier." },
@@ -20,15 +20,9 @@
20
20
  "profile": {
21
21
  "type": "string",
22
22
  "enum": ["BASELINE", "STANDARD", "HIGH", "CRITICAL"],
23
- "description": "Policy profile named by the caller. Takes the place of deriving one from `classification`; the verdict echoes the profile evaluated and stamps `classificationDefaulted: false`. Governance names BASELINE for kit-lane scans; the default when nothing is named is STANDARD."
24
- },
25
- "classification": {
26
- "type": "string",
27
- "enum": ["GREEN", "YELLOW", "ORANGE", "RED"],
28
- "description": "Data classification, mapped to a profile as in v1 (GREEN → BASELINE, YELLOW → STANDARD, ORANGE → HIGH, RED → CRITICAL). Not allowed beside `profile`. Absent with no `profile` means STANDARD and `classificationDefaulted: true`; an invalid value fails closed."
23
+ "description": "Policy profile the machine evaluates the evidence against; the verdict echoes it. Governance names BASELINE for kit-lane scans."
29
24
  }
30
25
  },
31
- "not": { "required": ["profile", "classification"] },
32
26
  "definitions": {
33
27
  "PackageSource": {
34
28
  "type": "object",
@@ -15,7 +15,7 @@
15
15
  "profile": {
16
16
  "type": "string",
17
17
  "enum": ["BASELINE", "STANDARD", "HIGH", "CRITICAL"],
18
- "description": "Policy profile the evidence was evaluated against: the input's `profile` when the caller named one, else derived from the input classification, else STANDARD."
18
+ "description": "Policy profile the evidence was evaluated against, echoed from the input's `profile`."
19
19
  },
20
20
  "reason": {
21
21
  "type": "string",
@@ -60,12 +60,21 @@
60
60
  "line": { "type": "integer", "minimum": 1, "description": "1-based line number, when the finding is line-anchored." },
61
61
  "message": { "type": "string", "minLength": 1, "description": "Human-readable finding description. Secret values are never included — only locations." },
62
62
  "remediation": { "type": "string", "description": "Suggested fix, when the tool provides one." },
63
- "package": { "type": "string", "minLength": 1, "description": "Dependency name, for a dependency-sca finding." },
64
- "installedVersion": { "type": "string", "minLength": 1, "description": "Installed version of `package` the vulnerability applies to." },
65
- "fixedVersion": { "type": "string", "minLength": 1, "description": "First version of `package` that fixes the vulnerability, when the tool knows one." },
63
+ "dependency": { "$ref": "#/definitions/Dependency" },
66
64
  "blocking": { "type": "boolean", "description": "Set by policy on every finding it evaluated: true when the finding on its own fires a block rule under the evaluated profile, false when it is reported only." }
67
65
  }
68
66
  },
67
+ "Dependency": {
68
+ "type": "object",
69
+ "additionalProperties": false,
70
+ "required": ["name", "installedVersion"],
71
+ "properties": {
72
+ "name": { "type": "string", "minLength": 1, "description": "Package name as the lockfile names it." },
73
+ "installedVersion": { "type": "string", "minLength": 1, "description": "Version of the package the app installs, which the vulnerability applies to." },
74
+ "fixedVersion": { "type": "string", "minLength": 1, "description": "The one version to upgrade to: the lowest fixed release on the installed version's major line, else the lowest fixed release above the installed version. Absent when no fix above the installed version exists." }
75
+ },
76
+ "description": "The dependency a dependency-sca finding is about. Absent on findings that are not about one installed package (secrets, code, a missing lockfile)."
77
+ },
69
78
  "EvidenceRef": {
70
79
  "type": "object",
71
80
  "additionalProperties": false,
@@ -78,7 +87,7 @@
78
87
  "Provenance": {
79
88
  "type": "object",
80
89
  "additionalProperties": false,
81
- "required": ["org", "app", "env", "snapshotSha256", "scannedAt", "toolManifestDigest", "scannerVersion", "classificationDefaulted"],
90
+ "required": ["org", "app", "env", "snapshotSha256", "scannedAt", "toolManifestDigest", "scannerVersion"],
82
91
  "properties": {
83
92
  "org": { "type": "string", "minLength": 1 },
84
93
  "app": { "type": "string", "minLength": 1 },
@@ -102,10 +111,6 @@
102
111
  "type": "string",
103
112
  "pattern": "^v?\\d+\\.\\d+\\.\\d+(?:[-+].*)?$",
104
113
  "description": "Orderable release version of the runner image that produced the evidence. Governance compares its numeric core with MINIMUM_SCANNER_VERSION to decide whether a PASS still counts."
105
- },
106
- "classificationDefaulted": {
107
- "type": "boolean",
108
- "description": "True iff the input named neither a profile nor a classification and the machine assumed STANDARD (decision 2026-08-27). An invalid classification fails closed and never reaches a verdict."
109
114
  }
110
115
  }
111
116
  }