@fourier-labs/harbour-contracts 0.46.0 → 0.50.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.
Files changed (40) hide show
  1. package/README.md +32 -6
  2. package/contract-release.json +30 -30
  3. package/dist/gen-ts/ai-policy-snapshot.d.ts +19 -0
  4. package/dist/gen-ts/ai-runtime-contract.d.ts +1 -1
  5. package/dist/gen-ts/ai-trace-event.d.ts +4 -1
  6. package/dist/gen-ts/app-integrations-declaration.d.ts +9 -2
  7. package/dist/gen-ts/contract-release.d.ts +6 -6
  8. package/dist/gen-ts/contract-release.js +6 -6
  9. package/dist/gen-ts/data-grant-v2.d.ts +60 -16
  10. package/dist/gen-ts/integration-error-details.d.ts +2 -2
  11. package/dist/gen-ts/integration-execute-request.d.ts +44 -4
  12. package/dist/gen-ts/integration-execute-result.d.ts +21 -1
  13. package/dist/gen-ts/saas-connection.d.ts +80 -29
  14. package/dist/gen-ts/transform-report.d.ts +2 -0
  15. package/dist/gen-ts/well-defined-app-manifest.d.ts +31 -0
  16. package/dist/resolver/index.d.ts +7 -1
  17. package/gen-ts/ai-policy-snapshot.ts +19 -0
  18. package/gen-ts/ai-runtime-contract.ts +1 -1
  19. package/gen-ts/ai-trace-event.ts +4 -1
  20. package/gen-ts/app-integrations-declaration.ts +9 -2
  21. package/gen-ts/contract-release.ts +6 -6
  22. package/gen-ts/data-grant-v2.ts +67 -16
  23. package/gen-ts/integration-error-details.ts +10 -2
  24. package/gen-ts/integration-execute-request.ts +50 -4
  25. package/gen-ts/integration-execute-result.ts +21 -1
  26. package/gen-ts/saas-connection.ts +87 -29
  27. package/gen-ts/transform-report.ts +2 -0
  28. package/gen-ts/well-defined-app-manifest.ts +31 -0
  29. package/package.json +1 -1
  30. package/schema/ai-policy-snapshot.schema.json +13 -1
  31. package/schema/ai-runtime-contract.schema.json +1 -1
  32. package/schema/ai-trace-event.schema.json +7 -1
  33. package/schema/app-integrations-declaration.schema.json +22 -6
  34. package/schema/data-grant-v2.schema.json +102 -23
  35. package/schema/integration-error-details.schema.json +10 -2
  36. package/schema/integration-execute-request.schema.json +53 -7
  37. package/schema/integration-execute-result.schema.json +36 -1
  38. package/schema/saas-connection.schema.json +97 -41
  39. package/schema/transform-report.schema.json +2 -1
  40. package/schema/well-defined-app-manifest.schema.json +61 -0
@@ -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 NOT a caller field: it is fixed by the operation and enforced from the approved grant; any identity/userId/scope field is rejected by additionalProperties. 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; warehouse.view.read {columns 1..32, limit? 1..1000, default 100}. 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 NOT a caller field: it is fixed by the operation and enforced from the approved grant; any identity/userId/scope field is rejected by additionalProperties. 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}. 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"],
@@ -16,18 +16,24 @@
16
16
  "properties": {
17
17
  "limit": { "type": "integer", "minimum": 1, "maximum": 1000 },
18
18
  "text": { "type": "string", "minLength": 1, "maxLength": 4000 },
19
+ "messageId": { "type": "string", "pattern": "^[A-Za-z0-9_-]{1,64}$", "description": "gmail.message.read: the Gmail message id a gmail.thread.list result named." },
20
+ "to": { "$ref": "#/definitions/EmailAddressList", "description": "gmail.message.send: primary recipients." },
21
+ "cc": { "$ref": "#/definitions/EmailAddressList", "description": "gmail.message.send: carbon-copy recipients." },
22
+ "bcc": { "$ref": "#/definitions/EmailAddressList", "description": "gmail.message.send: blind-copy recipients." },
23
+ "subject": { "type": "string", "minLength": 1, "maxLength": 256, "description": "gmail.message.send: one-line subject (no line breaks)." },
24
+ "replyToMessageId": { "type": "string", "pattern": "^[A-Za-z0-9_-]{1,64}$", "description": "gmail.message.send: a Gmail message id (from gmail.thread.list / gmail.message.read) the email replies to; the send joins that thread." },
19
25
  "columns": {
20
26
  "type": "array",
21
27
  "minItems": 1,
22
28
  "maxItems": 32,
23
- "items": { "type": "string", "pattern": "^[A-Za-z_][A-Za-z0-9_]{0,127}$" }
29
+ "items": { "$ref": "app-integrations-declaration.schema.json#/definitions/ColumnName" }
24
30
  }
25
31
  }
26
32
  },
27
33
  "idempotencyKey": {
28
34
  "type": "string",
29
35
  "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$",
30
- "description": "UUID of one explicit Send action; required for slack.message.post only. The same key with the same payload and subject returns the recorded outcome for seven days."
36
+ "description": "UUID of one explicit Send action; required for slack.message.post and gmail.message.send only. The same key with the same payload and subject returns the recorded outcome for seven days."
31
37
  }
32
38
  },
33
39
  "allOf": [
@@ -36,7 +42,7 @@
36
42
  "then": {
37
43
  "not": { "required": ["idempotencyKey"] },
38
44
  "properties": {
39
- "input": { "properties": { "limit": { "maximum": 15 } }, "not": { "anyOf": [{ "required": ["text"] }, { "required": ["columns"] }] } }
45
+ "input": { "properties": { "limit": { "maximum": 15 } }, "not": { "anyOf": [{ "required": ["text"] }, { "required": ["columns"] }, { "required": ["messageId"] }, { "required": ["to"] }, { "required": ["subject"] }] } }
40
46
  }
41
47
  }
42
48
  },
@@ -45,7 +51,7 @@
45
51
  "then": {
46
52
  "required": ["idempotencyKey"],
47
53
  "properties": {
48
- "input": { "required": ["text"], "not": { "anyOf": [{ "required": ["limit"] }, { "required": ["columns"] }] } }
54
+ "input": { "required": ["text"], "not": { "anyOf": [{ "required": ["limit"] }, { "required": ["columns"] }, { "required": ["messageId"] }, { "required": ["to"] }, { "required": ["subject"] }] } }
49
55
  }
50
56
  }
51
57
  },
@@ -54,11 +60,42 @@
54
60
  "then": {
55
61
  "not": { "required": ["idempotencyKey"] },
56
62
  "properties": {
57
- "input": { "required": ["columns"], "not": { "required": ["text"] } }
63
+ "input": { "required": ["columns"], "not": { "anyOf": [{ "required": ["text"] }, { "required": ["messageId"] }, { "required": ["to"] }, { "required": ["subject"] }] } }
64
+ }
65
+ }
66
+ },
67
+ {
68
+ "if": { "properties": { "operation": { "const": "gmail.thread.list" } } },
69
+ "then": {
70
+ "not": { "required": ["idempotencyKey"] },
71
+ "properties": {
72
+ "input": { "properties": { "limit": { "maximum": 15 } }, "not": { "anyOf": [{ "required": ["text"] }, { "required": ["columns"] }, { "required": ["messageId"] }, { "required": ["to"] }, { "required": ["subject"] }] } }
73
+ }
74
+ }
75
+ },
76
+ {
77
+ "if": { "properties": { "operation": { "const": "gmail.message.read" } } },
78
+ "then": {
79
+ "not": { "required": ["idempotencyKey"] },
80
+ "properties": {
81
+ "input": { "required": ["messageId"], "not": { "anyOf": [{ "required": ["text"] }, { "required": ["columns"] }, { "required": ["limit"] }, { "required": ["to"] }, { "required": ["subject"] }] } }
82
+ }
83
+ }
84
+ },
85
+ {
86
+ "if": { "properties": { "operation": { "const": "gmail.message.send" } } },
87
+ "then": {
88
+ "required": ["idempotencyKey"],
89
+ "properties": {
90
+ "input": { "required": ["to", "subject", "text"], "not": { "anyOf": [{ "required": ["limit"] }, { "required": ["columns"] }, { "required": ["messageId"] }] } }
58
91
  }
59
92
  }
60
93
  }
61
94
  ],
95
+ "definitions": {
96
+ "EmailAddress": { "type": "string", "minLength": 3, "maxLength": 254, "pattern": "^[^\\s@<>,;\"]+@[^\\s@<>,;\"]+\\.[^\\s@<>,;\"]+$", "description": "One bare email address (no display name)." },
97
+ "EmailAddressList": { "type": "array", "minItems": 1, "maxItems": 50, "uniqueItems": true, "items": { "$ref": "#/definitions/EmailAddress" } }
98
+ },
62
99
  "examples": [
63
100
  { "connection": "company-slack", "operation": "slack.channel.history", "resource": "team-updates", "input": { "limit": 15 } },
64
101
  {
@@ -68,6 +105,15 @@
68
105
  "input": { "text": "Weekly update posted." },
69
106
  "idempotencyKey": "6f1c2c1e-3a1c-4c3e-9f0e-2b6f7d8a9c10"
70
107
  },
71
- { "connection": "sales-warehouse", "operation": "warehouse.view.read", "resource": "weekly_sales", "input": { "columns": ["week", "total"], "limit": 100 } }
108
+ { "connection": "sales-warehouse", "operation": "warehouse.view.read", "resource": "weekly_sales", "input": { "columns": ["week", "total"], "limit": 100 } },
109
+ { "connection": "gmail", "operation": "gmail.thread.list", "resource": "inbox", "input": { "limit": 15 } },
110
+ { "connection": "gmail", "operation": "gmail.message.read", "resource": "inbox", "input": { "messageId": "18f2c3a4b5d6e7f8" } },
111
+ {
112
+ "connection": "gmail",
113
+ "operation": "gmail.message.send",
114
+ "resource": "inbox",
115
+ "input": { "to": ["jane@example.com"], "subject": "Weekly report", "text": "Shipped the weekly report.", "replyToMessageId": "18f2c3a4b5d6e7f8" },
116
+ "idempotencyKey": "6f1c2c1e-3a1c-4c3e-9f0e-2b6f7d8a9c11"
117
+ }
72
118
  ]
73
119
  }
@@ -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 three operation results: slack.channel.history -> {messages, hasMore} (plain text/IDs/time only, never the provider payload); slack.message.post -> {messageId}; 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 only, 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.",
6
6
  "type": "object",
7
7
  "additionalProperties": false,
8
8
  "properties": {
@@ -13,6 +13,19 @@
13
13
  },
14
14
  "hasMore": { "type": "boolean" },
15
15
  "messageId": { "type": "string", "minLength": 1, "maxLength": 128 },
16
+ "threads": {
17
+ "type": "array",
18
+ "maxItems": 15,
19
+ "items": { "$ref": "#/definitions/GmailThread" }
20
+ },
21
+ "id": { "type": "string", "minLength": 1, "maxLength": 128 },
22
+ "threadId": { "type": "string", "minLength": 1, "maxLength": 128 },
23
+ "subject": { "type": "string", "maxLength": 2048 },
24
+ "from": { "type": "string", "maxLength": 2048 },
25
+ "to": { "type": "string", "maxLength": 8192 },
26
+ "date": { "type": "string", "maxLength": 128 },
27
+ "text": { "type": "string", "maxLength": 16384 },
28
+ "truncated": { "type": "boolean" },
16
29
  "rows": {
17
30
  "type": "array",
18
31
  "maxItems": 1000,
@@ -22,9 +35,25 @@
22
35
  "oneOf": [
23
36
  { "required": ["messages", "hasMore"], "maxProperties": 2 },
24
37
  { "required": ["messageId"], "maxProperties": 1 },
38
+ { "required": ["messageId", "threadId"], "maxProperties": 2 },
39
+ { "required": ["threads", "hasMore"], "maxProperties": 2 },
40
+ { "required": ["id", "threadId", "subject", "from", "to", "date", "text", "truncated"], "maxProperties": 8 },
25
41
  { "required": ["rows"], "maxProperties": 1 }
26
42
  ],
27
43
  "definitions": {
44
+ "GmailThread": {
45
+ "type": "object",
46
+ "additionalProperties": false,
47
+ "required": ["id", "messageId", "subject", "from", "date", "snippet"],
48
+ "properties": {
49
+ "id": { "type": "string", "minLength": 1, "maxLength": 128 },
50
+ "messageId": { "type": "string", "minLength": 1, "maxLength": 128 },
51
+ "subject": { "type": "string", "maxLength": 2048 },
52
+ "from": { "type": "string", "maxLength": 2048 },
53
+ "date": { "type": "string", "maxLength": 128 },
54
+ "snippet": { "type": "string", "maxLength": 4096 }
55
+ }
56
+ },
28
57
  "ChannelMessage": {
29
58
  "type": "object",
30
59
  "additionalProperties": false,
@@ -43,6 +72,12 @@
43
72
  "hasMore": false
44
73
  },
45
74
  { "messageId": "1725840100.000200" },
75
+ { "messageId": "18f2c3a4b5d6e7f9", "threadId": "18f2c3a4b5d6e7f0" },
76
+ {
77
+ "threads": [{ "id": "18f2c3a4b5d6e7f0", "messageId": "18f2c3a4b5d6e7f8", "subject": "Weekly report", "from": "Jane <jane@example.com>", "date": "2026-09-11T08:00:00Z", "snippet": "Shipped the weekly report." }],
78
+ "hasMore": false
79
+ },
80
+ { "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 },
46
81
  { "rows": [{ "week": "2026-W36", "total": "1234.50" }] }
47
82
  ]
48
83
  }
@@ -2,70 +2,99 @@
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
3
  "$id": "https://schemas.harbour.dev/saas-connection.schema.json",
4
4
  "title": "SaasConnection",
5
- "description": "Tenant-wide SaaS connection record (control table sk SAASCONN#<sourceId>, same tenant partition as database sources; alias uniqueness is enforced across both). IT maps each environment to a provider installation and logical channel aliases to channel IDs; apps only ever see the aliases. Contains references, never secrets: the client secret lives in Secrets Manager under clientSecretRef and installation tokens under their own token records.",
5
+ "description": "Tenant-wide SaaS connection record (control table sk SAASCONN#<sourceId>, same tenant partition as database sources; alias uniqueness is enforced across both). One connection per tenant and provider: a single bot installation of the platform-owned provider app and a single logical channel map shared by every app environment (grants, requests and user consent links stay per environment). A Gmail connection has neither installation nor channels: user consent is the only step and its one resource is the fixed `inbox` alias. Contains references, never secrets: installation tokens live under their own token records. The pre-T25 per-environment shape (`clientId`, `clientSecretRef`, `environments`, `installations`) is deprecated: the control plane normalises such a stored record on read and never writes it.",
6
6
  "type": "object",
7
7
  "additionalProperties": false,
8
- "required": ["sourceId", "kind", "provider", "displayName", "status", "version", "clientId", "environments", "operations", "developmentPreapproval"],
8
+ "required": ["schema", "tenantId", "sourceId", "kind", "provider", "displayName", "status", "version", "channels", "operations", "developmentPreapproval", "createdAt", "updatedAt"],
9
9
  "properties": {
10
+ "schema": { "type": "string", "const": "harbour.saas-connection/1.0" },
11
+ "tenantId": { "type": "string", "minLength": 1, "maxLength": 255 },
10
12
  "sourceId": { "$ref": "app-integrations-declaration.schema.json#/definitions/IntegrationAlias" },
11
13
  "kind": { "type": "string", "const": "saas" },
12
- "provider": { "type": "string", "enum": ["slack"] },
13
- "displayName": { "type": "string", "minLength": 1, "maxLength": 255 },
14
+ "provider": { "$ref": "#/definitions/SaasProvider" },
15
+ "displayName": { "type": "string", "minLength": 1, "maxLength": 120 },
14
16
  "status": { "$ref": "#/definitions/SaasConnectionStatus" },
15
17
  "version": { "type": "integer", "minimum": 1, "description": "Optimistic-concurrency version; every write is conditional on it." },
16
- "clientId": { "type": "string", "minLength": 1, "maxLength": 255 },
18
+ "installation": { "$ref": "#/definitions/SaasInstallation" },
19
+ "channels": {
20
+ "type": "object",
21
+ "maxProperties": 64,
22
+ "propertyNames": { "$ref": "app-integrations-declaration.schema.json#/definitions/IntegrationAlias" },
23
+ "additionalProperties": { "type": "string", "pattern": "^[A-Z0-9]{5,20}$", "description": "Provider channel ID; only public, non-archived channels are valid." },
24
+ "description": "Logical channel alias -> provider channel ID, shared by every environment. Always empty for Gmail."
25
+ },
26
+ "operations": {
27
+ "type": "array",
28
+ "minItems": 1,
29
+ "maxItems": 5,
30
+ "uniqueItems": true,
31
+ "items": { "$ref": "app-integrations-declaration.schema.json#/definitions/IntegrationOperation" },
32
+ "not": { "contains": { "const": "warehouse.view.read" } },
33
+ "description": "Operations IT allows apps to request on this connection: the provider's own SaaS operations (Slack: slack.channel.history, slack.message.post; Gmail: gmail.thread.list, gmail.message.read, gmail.message.send). Never warehouse.view.read."
34
+ },
35
+ "developmentPreapproval": { "$ref": "#/definitions/DevelopmentPreapproval" },
36
+ "blockedDevelopmentBuilders": {
37
+ "type": "array",
38
+ "maxItems": 256,
39
+ "uniqueItems": true,
40
+ "items": { "type": "string", "minLength": 1, "maxLength": 255 },
41
+ "description": "Lower-cased builder emails excluded from development preapproval."
42
+ },
43
+ "createdAt": { "type": "string", "format": "date-time" },
44
+ "updatedAt": { "type": "string", "format": "date-time" },
45
+ "clientId": { "type": "string", "minLength": 1, "maxLength": 255, "deprecated": true, "description": "Deprecated (pre-T25 tenant-owned provider client). Dropped on read; never written." },
17
46
  "clientSecretRef": {
18
47
  "type": "string",
19
48
  "pattern": "^secretref://[A-Za-z0-9][A-Za-z0-9/_.:-]*$",
20
- "description": "secretref://tenant/{tenantId}/saas/{sourceId}/client"
49
+ "deprecated": true,
50
+ "description": "Deprecated (pre-T25 tenant-owned provider client secret reference). Dropped on read; never written."
21
51
  },
22
52
  "environments": {
23
53
  "type": "object",
24
54
  "additionalProperties": false,
25
55
  "minProperties": 1,
56
+ "deprecated": true,
57
+ "description": "Deprecated pre-T25 per-environment mapping. On read the development entry's channels become `channels`; never written.",
26
58
  "properties": {
27
59
  "development": { "$ref": "#/definitions/SaasEnvironmentMapping" },
28
60
  "preview": { "$ref": "#/definitions/SaasEnvironmentMapping" },
29
61
  "production": { "$ref": "#/definitions/SaasEnvironmentMapping" }
30
62
  }
31
63
  },
32
- "operations": {
33
- "type": "array",
34
- "minItems": 1,
35
- "maxItems": 2,
36
- "uniqueItems": true,
37
- "items": { "type": "string", "enum": ["slack.channel.history", "slack.message.post"] },
38
- "description": "Operations IT allows apps to request on this connection."
39
- },
40
- "developmentPreapproval": { "$ref": "#/definitions/DevelopmentPreapproval" },
41
64
  "installations": {
42
65
  "type": "object",
43
66
  "propertyNames": { "$ref": "#/definitions/InstallationName" },
44
- "additionalProperties": { "$ref": "#/definitions/SaasInstallation" },
45
- "description": "Provider installations keyed by the installation name environments point at."
46
- },
47
- "createdAt": { "type": "string", "format": "date-time" },
48
- "updatedAt": { "type": "string", "format": "date-time" }
67
+ "additionalProperties": { "$ref": "#/definitions/LegacySaasInstallation" },
68
+ "deprecated": true,
69
+ "description": "Deprecated pre-T25 named installations. On read the `nonprod` entry (or the development mapping's) becomes `installation`; never written."
70
+ }
49
71
  },
50
72
  "definitions": {
73
+ "SaasProvider": {
74
+ "type": "string",
75
+ "enum": ["slack", "gmail"],
76
+ "description": "Slack carries an installation and a channel map; Gmail is user-consent only and read-only."
77
+ },
51
78
  "SaasConnectionStatus": {
52
79
  "type": "string",
53
80
  "enum": ["ACTIVE", "DISABLED"],
54
81
  "description": "DISABLED stops every app call immediately (details.code CONNECTION_DISABLED)."
55
82
  },
56
- "InstallationName": { "type": "string", "pattern": "^[a-z][a-z0-9_-]{0,62}$" },
57
- "SaasEnvironmentMapping": {
83
+ "SaasInstallationStatus": {
84
+ "type": "string",
85
+ "enum": ["installed", "reconnect_required", "revoked"]
86
+ },
87
+ "SaasInstallation": {
58
88
  "type": "object",
59
89
  "additionalProperties": false,
60
- "required": ["installation", "channels"],
90
+ "required": ["tokenRecordId", "workspaceId", "botUserId", "status", "installedAt"],
91
+ "description": "The one bot installation of the platform-owned provider app in this tenant's workspace.",
61
92
  "properties": {
62
- "installation": { "$ref": "#/definitions/InstallationName" },
63
- "channels": {
64
- "type": "object",
65
- "maxProperties": 64,
66
- "propertyNames": { "$ref": "app-integrations-declaration.schema.json#/definitions/IntegrationAlias" },
67
- "additionalProperties": { "type": "string", "pattern": "^[A-Z0-9]{1,32}$", "description": "Provider channel ID; only public, non-archived channels are valid." }
68
- }
93
+ "tokenRecordId": { "type": "string", "minLength": 1, "maxLength": 128 },
94
+ "workspaceId": { "type": "string", "minLength": 1, "maxLength": 128 },
95
+ "botUserId": { "type": "string", "maxLength": 128, "description": "Empty only for a legacy record normalised without one." },
96
+ "status": { "$ref": "#/definitions/SaasInstallationStatus" },
97
+ "installedAt": { "type": "string", "format": "date-time" }
69
98
  }
70
99
  },
71
100
  "DevelopmentPreapproval": {
@@ -77,10 +106,26 @@
77
106
  "hours": { "type": "integer", "minimum": 1, "maximum": 168, "description": "Development grant duration; default 24." }
78
107
  }
79
108
  },
80
- "SaasInstallation": {
109
+ "InstallationName": { "type": "string", "pattern": "^[a-z][a-z0-9_-]{0,62}$", "deprecated": true },
110
+ "SaasEnvironmentMapping": {
81
111
  "type": "object",
82
112
  "additionalProperties": false,
83
- "required": ["tokenRecordId", "workspaceId", "botUserId", "status"],
113
+ "required": ["installation", "channels"],
114
+ "deprecated": true,
115
+ "properties": {
116
+ "installation": { "$ref": "#/definitions/InstallationName" },
117
+ "channels": {
118
+ "type": "object",
119
+ "maxProperties": 64,
120
+ "propertyNames": { "$ref": "app-integrations-declaration.schema.json#/definitions/IntegrationAlias" },
121
+ "additionalProperties": { "type": "string", "pattern": "^[A-Z0-9]{1,32}$" }
122
+ }
123
+ }
124
+ },
125
+ "LegacySaasInstallation": {
126
+ "type": "object",
127
+ "additionalProperties": false,
128
+ "deprecated": true,
84
129
  "properties": {
85
130
  "tokenRecordId": { "type": "string", "minLength": 1, "maxLength": 128 },
86
131
  "workspaceId": { "type": "string", "minLength": 1, "maxLength": 128 },
@@ -91,25 +136,36 @@
91
136
  },
92
137
  "examples": [
93
138
  {
139
+ "schema": "harbour.saas-connection/1.0",
140
+ "tenantId": "fourier",
94
141
  "sourceId": "company-slack",
95
142
  "kind": "saas",
96
143
  "provider": "slack",
97
144
  "displayName": "Company Slack",
98
145
  "status": "ACTIVE",
99
146
  "version": 3,
100
- "clientId": "1234567890.1234567890",
101
- "clientSecretRef": "secretref://tenant/fourier/saas/company-slack/client",
102
- "environments": {
103
- "development": { "installation": "nonprod", "channels": { "team-updates": "C0123NONPROD" } },
104
- "production": { "installation": "prod", "channels": { "team-updates": "C0123PROD" } }
105
- },
147
+ "installation": { "tokenRecordId": "tok_workspace", "workspaceId": "T0WORKSPACE", "botUserId": "U0BOT", "status": "installed", "installedAt": "2026-09-09T08:00:00Z" },
148
+ "channels": { "team-updates": "C0123TEAM" },
106
149
  "operations": ["slack.channel.history", "slack.message.post"],
107
150
  "developmentPreapproval": { "enabled": true, "hours": 24 },
108
- "installations": {
109
- "nonprod": { "tokenRecordId": "tok_nonprod", "workspaceId": "T0NONPROD", "botUserId": "U0BOT", "status": "ACTIVE" }
110
- },
111
151
  "createdAt": "2026-09-09T08:00:00Z",
112
152
  "updatedAt": "2026-09-09T08:00:00Z"
153
+ },
154
+ {
155
+ "schema": "harbour.saas-connection/1.0",
156
+ "tenantId": "fourier",
157
+ "sourceId": "gmail",
158
+ "kind": "saas",
159
+ "provider": "gmail",
160
+ "displayName": "Gmail",
161
+ "status": "ACTIVE",
162
+ "version": 1,
163
+ "channels": {},
164
+ "operations": ["gmail.thread.list", "gmail.message.read", "gmail.message.send"],
165
+ "developmentPreapproval": { "enabled": false, "hours": 24 },
166
+ "blockedDevelopmentBuilders": ["contractor@example.com"],
167
+ "createdAt": "2026-09-11T08:00:00Z",
168
+ "updatedAt": "2026-09-11T08:00:00Z"
113
169
  }
114
170
  ]
115
171
  }
@@ -9,6 +9,7 @@
9
9
  "required": ["application"],
10
10
  "properties": {
11
11
  "applied_playbooks": {
12
+ "description": "Optional self-report of the playbooks the agent says it applied. A claim about its own behaviour is not evidence, so nothing validates it; a deployable report may omit it, and one that still carries it stays valid.",
12
13
  "type": "array",
13
14
  "minItems": 1,
14
15
  "items": { "type": "string", "minLength": 1 }
@@ -666,7 +667,7 @@
666
667
  ],
667
668
  "oneOf": [
668
669
  {
669
- "required": ["applied_playbooks", "static_rebuild_required", "analysis", "database", "preservation_plan", "validation_fixtures", "application"],
670
+ "required": ["static_rebuild_required", "analysis", "database", "preservation_plan", "validation_fixtures", "application"],
670
671
  "properties": {
671
672
  "application": {
672
673
  "required": ["runtime", "buildKind", "topology", "entrypoint", "build", "basePath", "evidence", "unsupportedFeatures"],
@@ -26,9 +26,70 @@
26
26
  "type": "array",
27
27
  "maxItems": 128,
28
28
  "items": { "$ref": "#/definitions/WorkloadBinding" }
29
+ },
30
+ "runtime": {
31
+ "type": "string",
32
+ "pattern": "^(?:nodejs|python)[0-9]+(?:\\.[0-9]+)?$",
33
+ "description": "Optional language runtime the application container is built on, spelled exactly as transform-report.application.runtime does (nodejs22, python3.11). Optional so a manifest written before the producer emits it still validates."
34
+ },
35
+ "capabilities": {
36
+ "type": "array",
37
+ "maxItems": 5,
38
+ "uniqueItems": true,
39
+ "items": { "$ref": "#/definitions/Capability" },
40
+ "description": "Optional declaration of the Harbour browser capabilities the application uses. Same closed set as the app-capabilities document; this is the declaration, not a source scan."
41
+ },
42
+ "routeAuth": {
43
+ "$ref": "#/definitions/RouteAuth",
44
+ "description": "Optional method/path authentication policy. Omitted means every route requires Harbour SSO, the same default the policy states explicitly."
29
45
  }
30
46
  },
31
47
  "definitions": {
48
+ "Capability": {
49
+ "type": "string",
50
+ "enum": ["data", "files", "actions", "realtime", "telemetry"]
51
+ },
52
+ "RouteAuth": {
53
+ "type": "object",
54
+ "additionalProperties": false,
55
+ "required": ["default", "public"],
56
+ "description": "Harbour SSO is required by default; the public list is the only way a route escapes it, and each entry names the methods and the application-authorized mode so the artifact contract's route_auth can be built from this document alone.",
57
+ "properties": {
58
+ "default": {
59
+ "type": "string",
60
+ "const": "sso_required",
61
+ "default": "sso_required"
62
+ },
63
+ "public": {
64
+ "type": "array",
65
+ "maxItems": 64,
66
+ "default": [],
67
+ "items": { "$ref": "#/definitions/PublicRoute" }
68
+ }
69
+ }
70
+ },
71
+ "PublicRoute": {
72
+ "type": "object",
73
+ "additionalProperties": false,
74
+ "required": ["path", "methods", "mode"],
75
+ "properties": {
76
+ "path": {
77
+ "type": "string",
78
+ "pattern": "^/(?:[A-Za-z0-9._~-]+/)*(?:[A-Za-z0-9._~-]+|\\*)$"
79
+ },
80
+ "methods": {
81
+ "type": "array",
82
+ "minItems": 1,
83
+ "maxItems": 7,
84
+ "uniqueItems": true,
85
+ "items": {
86
+ "type": "string",
87
+ "enum": ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]
88
+ }
89
+ },
90
+ "mode": { "type": "string", "const": "application_authorized" }
91
+ }
92
+ },
32
93
  "ResourceProfile": {
33
94
  "type": "string",
34
95
  "enum": ["standard", "memory-1g", "memory-2g"]