@fourier-labs/harbour-contracts 0.51.0 → 0.53.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.
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "schema": "harbour.contract-release/1.0",
3
- "repositoryVersion": "0.51.0.0",
4
- "publishVersion": "0.51.0",
5
- "publishTag": "v0.51.0",
6
- "schemasDigest": "sha256:018bf3078087a18b07990772c94669f658159bc84b0c8accedc7dd5daa38bee5",
3
+ "repositoryVersion": "0.53.0.0",
4
+ "publishVersion": "0.53.0",
5
+ "publishTag": "v0.53.0",
6
+ "schemasDigest": "sha256:c4d7c9642842bf3709003f447fb22429ce3573acabaa2da8279e0fc399e8d237",
7
7
  "aiFixturesDigest": "sha256:edac31c92e146ca1c40130f2fb5649db82e24b8cd1ac758153b90612052fddb6",
8
- "generatedGoDigest": "sha256:98bb6999738be926c36fd8d0735a17defdad590ced57e94bc431bb9bba53c9ad",
9
- "generatedTypeScriptDigest": "sha256:1d83b04e4899777f2963177e39e7ac99852cae80ffb8c03b6a294f5fb19d3600",
8
+ "generatedGoDigest": "sha256:8f3c3281a76792a9480ca1f7bcbad820db1908efffb29072fcdb0a8f99dbd208",
9
+ "generatedTypeScriptDigest": "sha256:82a083862e500a06df3550e694221cff9dcb98461d5174889c374713f961040c",
10
10
  "generatedExclusions": {
11
11
  "gen-go/contract_release.go": "contains the generated aggregate digests (self-reference)",
12
12
  "gen-ts/contract-release.ts": "contains the generated aggregate digests (self-reference)"
@@ -31,12 +31,12 @@
31
31
  "schema/ai-transform-result.schema.json": "sha256:244e380a36957ab2dcb526ea22aaecb56b53664ce1fda6934d0021c084c99133",
32
32
  "schema/ai-verification-evidence.schema.json": "sha256:bdbc762df256d2a8dd0bdda60d4d13915ce40034cd756af9515871d145f10ad9",
33
33
  "schema/api-error-response.schema.json": "sha256:3d43d5bdf0b59c944e20509d832e6776c3ecdba4d6c245a9118bbe52062a8304",
34
- "schema/app-integrations-declaration.schema.json": "sha256:1a6ac2e7d77a4d4dcbce8b937747f125081413f819ffee9e27e8b950a436cb22",
34
+ "schema/app-integrations-declaration.schema.json": "sha256:5d43c31719c93c9c8235b6c03dbe55a42ebdb2efb78225ad14222bd1086dcf6a",
35
35
  "schema/artifact-contract.schema.json": "sha256:8047b461a1c6d07bbe867a259c57bdf5cc0fdf282cf842ec7b683a9e93713b10",
36
36
  "schema/binding-classification-subject.schema.json": "sha256:a8825ce652e7e00a18759c4249bd5bcd0cae552dd58440513f867e87f685133c",
37
37
  "schema/builder-input-proposal.schema.json": "sha256:fe85cffa6d76b1e1effe1f5d75525f8fe0e1eaa3be4a304e491aa23ad72c0e72",
38
38
  "schema/data-dependency.schema.json": "sha256:89ba8481ccabc935e7dab5d13871c985839a751ad1586710d8382a15e1f3340c",
39
- "schema/data-grant-v2.schema.json": "sha256:6d639745e7a00f5f6383fa9034e3b04b6772cb29a60a6db1a3c517dc7a6c26ba",
39
+ "schema/data-grant-v2.schema.json": "sha256:7fa6022aa2fcfd3c94bb969315155ea24e5a1aa25a25cb718df4465397076e07",
40
40
  "schema/data-grant.schema.json": "sha256:e640553831ecc1c01cc9a37fe388b84d0ffeaa0d0050e63712b48f65fa437c8a",
41
41
  "schema/data-source.schema.json": "sha256:aec8d2497dae9a91886a3df9f6fbbdb3d3fcab44b14fbcacc3f5e99b827f1bba",
42
42
  "schema/deploy-request.schema.json": "sha256:32531e21983cdfeec511f8b4cddd4dad6530cd90e4e9fe147225d78d138b6198",
@@ -52,20 +52,20 @@
52
52
  "schema/error-envelope.schema.json": "sha256:75dccda7dd16fdf8435deda53fc7dbcdc109b65114617a17f11763010c656f90",
53
53
  "schema/integration-connect-result.schema.json": "sha256:3f254b67ef739dc1fa395bdc8c4dc93359b16d481dba2a2d777aa5b6f34b7af8",
54
54
  "schema/integration-error-details.schema.json": "sha256:09cebaddb7f88e63cbebaa1cada5f91057ae4dc4370e973ef38f7f11c6323284",
55
- "schema/integration-execute-request.schema.json": "sha256:18613bab7c233d1e2d52b960f5fe6ae4409171fa03a3aebf415cf86ae66e2866",
55
+ "schema/integration-execute-request.schema.json": "sha256:efbf25e2c01ccafec6e5c8023d891daa51e8b8c29a1735afe0f0eb33c4615834",
56
56
  "schema/integration-execute-result.schema.json": "sha256:766146779c01ee1dbdd2161c5dc594f43ccaf322762352bb0dc5b80bcb3275b5",
57
57
  "schema/kit-bundle-manifest.schema.json": "sha256:c15e28ae028dbd1c262427b9c797142e19b1f38fa36b4ecfe187e32c20a174ec",
58
58
  "schema/kit-lock.schema.json": "sha256:5e24b76660f83990af7a6c037af350c3850ee439ef956f65a0360facbdcdd997",
59
59
  "schema/kvs-routing-entry.schema.json": "sha256:b75f3a5dfa93a428b2764e03222b66b6ef2bed980f8bac8ffac54afdf3526bc5",
60
60
  "schema/promotion-request.schema.json": "sha256:00e96dfd8ddbdba88ea222941d43f3609ff6c815a79e34f6705cab458101c969",
61
61
  "schema/promotion-response.schema.json": "sha256:4129712d2e29d6ef21d93d4bd0bacfd60ada3f2ee0ade852824e64346b817634",
62
- "schema/saas-connection.schema.json": "sha256:fa62fc6e28124120c71ebc36e257263ba8820d48b7d505d54ac53bd812fdd9ee",
62
+ "schema/saas-connection.schema.json": "sha256:1fcb1a3ee922e49d8f846db6bd6284f6de483b26cb1ae03da0381b396bb8132d",
63
63
  "schema/sanitized-binding-evidence.schema.json": "sha256:1191ab88e2764df927e5f17837674cebebe068d480da2b2fdb390e589f96b51f",
64
64
  "schema/secret-reference.schema.json": "sha256:db4e3c2c6a2dbd72d7ea0f9c91cb3b2343a7e67b601996c6818b2ef08e440895",
65
65
  "schema/security-scan-input.schema.json": "sha256:83502dffb3c07ac2e0698928e6dc123f454fc667d4aacd336bcad6944ef2ac3d",
66
66
  "schema/security-verdict.schema.json": "sha256:ec3c2d5c9df3601aeed0909b36c7f427469c7f0eeb3295c5809b96d09bf3fc26",
67
67
  "schema/transform-parked-result.schema.json": "sha256:11729980d4c89d4a9e18f5f213232cd3359739c85626b36c20c2a37ba794c9af",
68
- "schema/transform-report.schema.json": "sha256:858da48fa7fe97d8d06267bb3435d71ba307133b246cb020f5b473bb488c445a",
68
+ "schema/transform-report.schema.json": "sha256:62c8f3aa3ac748cb54d78cb729bdfbaaccf1c03574ea92841eb23b2557eff3cf",
69
69
  "schema/version-state.schema.json": "sha256:3b190007e2f8e3821c41ef11c6f5b226da7d5a18d34d4077f4618004846fd444",
70
70
  "schema/well-defined-app-manifest.schema.json": "sha256:40f1bfea912dd85a05a0a3be2f6becadaf8ae3c5de9d5782ff1c8dd1bd83485f"
71
71
  },
@@ -107,8 +107,8 @@
107
107
  "gen-go/error_category.go": "sha256:b2ddea22dcfa030f1a45e721e635485b0bd816cf1f38e7654628a653f5ed4583",
108
108
  "gen-go/integration_execute_input_rules.go": "sha256:224fb490a5f936b30927e6637e698093e1e42a695cbc1bcf9a39bc70db9f2172",
109
109
  "gen-go/integration_execute_input_rules_test.go": "sha256:c437a8406a2f062a0173b9419e4cb176fc6eb54e569506225d5162323f55b40b",
110
- "gen-go/schema_documents.go": "sha256:66055e0d4354f73d922ee17984de8df065db665f93182b87fcd8a6a94ae94300",
111
- "gen-go/types.go": "sha256:c94b302d560efb7d8a8f6a4387a22a2f0321489e04d9d126d561cd921ff07eec",
110
+ "gen-go/schema_documents.go": "sha256:a01b157658d23e2585e64ff101ed720942856626676b50c83efbd8b044a050d6",
111
+ "gen-go/types.go": "sha256:819a27018a274c83233993a0c54359219ebbd0b11523f68c3c34ba0223d9e5ba",
112
112
  "gen-go/version_state.go": "sha256:4ef8abb126414c0025cf2c59bc9c79a74ef4a2ff89e8f316cbf05acc872371c1"
113
113
  },
114
114
  "generatedTypeScript": {
@@ -133,13 +133,13 @@
133
133
  "gen-ts/ai-transform-result.ts": "sha256:208c66b2aeb425c0cbcc18b7191f6e8989443f2b2c1690648e0e06cc1448ce25",
134
134
  "gen-ts/ai-verification-evidence.ts": "sha256:979439d77f8b47631d1eb3b6c3c54ea6fec41089d2a51dc815fd370b1c4917fc",
135
135
  "gen-ts/api-error-response.ts": "sha256:0fd73be282e7a5d72d6c343a4e64d09c232556b16161167f0cffd6278ed7e3d3",
136
- "gen-ts/app-integrations-declaration.ts": "sha256:aabebdbffc70dd87017f1c72e0b8713f9cef506ba5258279b3a1405185875141",
136
+ "gen-ts/app-integrations-declaration.ts": "sha256:c2edea82c2c80d15dc233499ef289ea310e59b61b1e4428ea9e4fed85168d2a4",
137
137
  "gen-ts/artifact-contract.ts": "sha256:4a6dc6d2caaeed4bb2066cc8a333e6e09e5a0d4745e0b1038293d54ab9403aeb",
138
138
  "gen-ts/binding-classification-digest-helper.ts": "sha256:40fc7d85b4c3f3ffc5caf7d7950c62d013c83ee42058c7453b2100d95817f167",
139
139
  "gen-ts/binding-classification-subject.ts": "sha256:c409a3fd7b942b50419632c51d9a79474fb30d6374bdc79c0e25c73618e800c3",
140
140
  "gen-ts/builder-input-proposal.ts": "sha256:b0108c3787873d3cf1a39f00967379418a6766275d3474cfa55f5a9d93ead174",
141
141
  "gen-ts/data-dependency.ts": "sha256:0572c31d7ae9088eccc104eee1a205bf176d1d6dacfaf0f290d666024e284966",
142
- "gen-ts/data-grant-v2.ts": "sha256:d55491e8d1a73d8999a43904344760de9c6eb2ce500b1e942d49261a9c67808f",
142
+ "gen-ts/data-grant-v2.ts": "sha256:d7c78866dfbad6c852bdbf470057a7b926edcadf5563e6ea4da3da9ae96cf699",
143
143
  "gen-ts/data-grant.ts": "sha256:2358579d8aa1e57b01a6f2b0eacce5531e7f21ef226404c9809925365e0d6155",
144
144
  "gen-ts/data-source.ts": "sha256:77d6bb3b0eaa355789c5d97dd1f548734add6950329dc69549350b8d17c8d582",
145
145
  "gen-ts/deploy-request.ts": "sha256:0214faa8be6680f0bfc117d5ce67c1a59fcc18bd0ea4bc840e03e7d051fa9ab6",
@@ -158,21 +158,21 @@
158
158
  "gen-ts/integration-connect-result.ts": "sha256:4b10f169b6e1af364e9b418de21caa9fadbfb77b43cbff77d741cfae0b52180c",
159
159
  "gen-ts/integration-error-details.ts": "sha256:8243f5871f376d1cab4d90e07f3151d148fb03c704168dbccdfe6c9fe76bce83",
160
160
  "gen-ts/integration-execute-input-rules.ts": "sha256:a779c872c44bb892dec7b28173d961696d95e5b6e1fa14e2ba67dd78344cdc5b",
161
- "gen-ts/integration-execute-request.ts": "sha256:06056ac8ca4a649560189ee78b40a9d5ad5ca5089d967e5708cbc082aa21ba95",
161
+ "gen-ts/integration-execute-request.ts": "sha256:b80260f8cca8da13692a6e0fa5de62b82c6eb48b0f3f3664f44f086441a519db",
162
162
  "gen-ts/integration-execute-result.ts": "sha256:4aaf6a9688f9dfdbc87a408556b62f822518acf5b76548076adb55393c0863b4",
163
163
  "gen-ts/kit-bundle-manifest.ts": "sha256:9041b32af3df781ca6800bc14c3719e5e54c81dff370633454ac3ad27432eb58",
164
164
  "gen-ts/kit-lock.ts": "sha256:de4ae82bac7bd234a3230ae671962e80eed6542d695a032a442673659dc6be5e",
165
165
  "gen-ts/kvs-routing-entry.ts": "sha256:d92e85ee9f257d77850d1d3aac9438e2055f38084ff84a48fcedae239d47d4ca",
166
166
  "gen-ts/promotion-request.ts": "sha256:9db88cef67f777a16bac628039ec3f582acb9e8cfff6fb592b30d0a0e55abc68",
167
167
  "gen-ts/promotion-response.ts": "sha256:f1c9cafac96faeb02a443a31786f67714e818f38d90108e7af41958396524d6f",
168
- "gen-ts/saas-connection.ts": "sha256:62b5c5ffd125099bae46142013461c9ac3f75c2d27ab72cd4842a937f3e2f308",
168
+ "gen-ts/saas-connection.ts": "sha256:b60ab802c73edfe805d59d55a70ecd54fe569646d238f5ff213d26ab34b717f1",
169
169
  "gen-ts/sanitized-binding-digest-helper.ts": "sha256:c47d7fdc20500e2e72c23fe39ecdd610fc397c47fcba51335ebec3c380fe6872",
170
170
  "gen-ts/sanitized-binding-evidence.ts": "sha256:f3fb7b1d7d2e0929eb0ea981eaf32db3104010647185f6ae59f67167d42538a9",
171
171
  "gen-ts/secret-reference.ts": "sha256:731fbcde7315cb4128d5b01bdbe6e6f248f13c7ad8dea04a3a4e927ac4890198",
172
172
  "gen-ts/security-scan-input.ts": "sha256:b31f048be04c34b40f49d3b3ba53277e473cd435ae9216ba3a3b4dac69fb5685",
173
173
  "gen-ts/security-verdict.ts": "sha256:fea950cddbd12fe0218e2573c9a29c6748d56612a464559f1e6d688fa97f68c7",
174
174
  "gen-ts/transform-parked-result.ts": "sha256:6279e27cee8d75c29f3ede45580fef1daa0ca300867a78e43ead8a7689ec613f",
175
- "gen-ts/transform-report.ts": "sha256:005a10b34b89f055c3aae4bf54c095bbca7a9e15bd9b406b61a9eaafffc37a61",
175
+ "gen-ts/transform-report.ts": "sha256:2f42c94644b0f4e2d03e339d0b9d333254555746f74d362d820522156e964ef9",
176
176
  "gen-ts/version-state-machine.ts": "sha256:c67af3e86ac306743345b850dfd3ae5201c19d10c40e2c149cf8c38258320d5f",
177
177
  "gen-ts/version-state.ts": "sha256:a835112930299e7b593a1b8810b7414f6c9e809da2092306c743128170a3b702",
178
178
  "gen-ts/well-defined-app-manifest.ts": "sha256:2b72a39c2fa8e76239634d0478dc0da05446e74e944950eacf999b78876455c6"
@@ -8,12 +8,16 @@ export type IntegrationAlias = string;
8
8
  * @maxItems 64
9
9
  */
10
10
  export type ResourceNames = IntegrationAlias[];
11
+ /**
12
+ * Whose authority an operation runs under. Fixed per operation (slack.message.post: declared per app); never chosen at call time.
13
+ */
14
+ export type IntegrationIdentityMode = 'app' | 'user';
11
15
  /**
12
16
  * A plain column identifier of a warehouse view.
13
17
  */
14
18
  export type ColumnName = string;
15
19
  /**
16
- * Committed `.harbour/integrations.json`: the app's declared use of tenant-wide integration connections. Keys are logical connection aliases (the governance sourceId); resources are logical names (Slack channel alias, warehouse view name), never IDs, tokens or URLs. The operation set is closed (five operations) and each operation carries a fixed identity mode: slack.channel.history=user, slack.message.post=app, gmail.thread.list=user, gmail.message.read=user, warehouse.view.read=app. A `saas` connection may declare only the Slack and Gmail operations and a `database` connection only warehouse.view.read. Gmail is read-only and user-mode only; its single resource is the fixed `inbox` alias. This file also owns the shared IntegrationOperation / IntegrationIdentityMode / IntegrationEnvironment / IntegrationConnectionKind enums referenced by the other integration schemas.
20
+ * Committed `.harbour/integrations.json`: the app's declared use of tenant-wide integration connections. Keys are logical connection aliases (the governance sourceId); resources are logical names (Slack channel alias, warehouse view name), never IDs, tokens or URLs. The operation set is closed (six operations). Each operation carries a fixed identity mode — slack.channel.history=user, gmail.thread.list=user, gmail.message.read=user, gmail.message.send=user, warehouse.view.read=app — except slack.message.post, whose mode the app DECLARES: `app` posts as the company's Slack app (the one audited bot identity, under the IT-approved per-app display name when the connection declares `presentation`), `user` posts as the signed-in person after their own consent. The mode is never chosen at call time: governance runs the call under the approved grant of the declared mode. A `saas` connection may declare only the Slack and Gmail operations and a `database` connection only warehouse.view.read; `presentation` is a `saas` connection's optional per-app display name and icon for app-mode Slack posts, shown to IT at approval time and applied only from the approved grant. Gmail is user-mode only (reads plus one send); its single resource is the fixed `inbox` alias. This file also owns the shared IntegrationOperation / IntegrationIdentityMode / IntegrationEnvironment / IntegrationConnectionKind / IntegrationPresentation definitions referenced by the other integration schemas.
17
21
  */
18
22
  export interface AppIntegrationsDeclaration {
19
23
  schema: 'harbour.app-integrations/2.0';
@@ -23,21 +27,38 @@ export interface AppIntegrationsDeclaration {
23
27
  }
24
28
  export interface DeclaredConnection {
25
29
  kind: IntegrationConnectionKind;
30
+ presentation?: IntegrationPresentation;
26
31
  operations: {
27
32
  'slack.channel.history'?: UserModeChannelOperation;
28
- 'slack.message.post'?: AppModeChannelOperation;
33
+ 'slack.message.post'?: DeclaredModeChannelOperation;
29
34
  'gmail.thread.list'?: UserModeChannelOperation;
30
35
  'gmail.message.read'?: UserModeChannelOperation;
31
36
  'gmail.message.send'?: UserModeChannelOperation;
32
37
  'warehouse.view.read'?: AppModeViewOperation;
33
38
  };
34
39
  }
40
+ /**
41
+ * saas connections only (Slack): how this app's app-mode posts are presented; meaningless for every other operation.
42
+ */
43
+ export interface IntegrationPresentation {
44
+ /**
45
+ * 1..80 characters of plain text: not blank, no control characters, no @-mentions (`@name`, `<@id>`). Must not impersonate a person: IT sees it before approving.
46
+ */
47
+ displayName: string;
48
+ /**
49
+ * A Slack emoji shortcode such as `:sandwich:`; absent keeps the bot's own icon.
50
+ */
51
+ iconEmoji?: string;
52
+ }
35
53
  export interface UserModeChannelOperation {
36
54
  identity: 'user';
37
55
  resources: ResourceNames;
38
56
  }
39
- export interface AppModeChannelOperation {
40
- identity: 'app';
57
+ /**
58
+ * slack.message.post: the app declares the mode — app (posts as the company's Slack app) or user (posts as the signed-in person).
59
+ */
60
+ export interface DeclaredModeChannelOperation {
61
+ identity: IntegrationIdentityMode;
41
62
  resources: ResourceNames;
42
63
  }
43
64
  export interface AppModeViewOperation {
@@ -1,7 +1,7 @@
1
- export declare const ContractRepositoryVersion: "0.51.0.0";
2
- export declare const ContractPublishVersion: "0.51.0";
3
- export declare const ContractPublishTag: "v0.51.0";
4
- export declare const ContractSchemasDigest: "sha256:018bf3078087a18b07990772c94669f658159bc84b0c8accedc7dd5daa38bee5";
1
+ export declare const ContractRepositoryVersion: "0.53.0.0";
2
+ export declare const ContractPublishVersion: "0.53.0";
3
+ export declare const ContractPublishTag: "v0.53.0";
4
+ export declare const ContractSchemasDigest: "sha256:c4d7c9642842bf3709003f447fb22429ce3573acabaa2da8279e0fc399e8d237";
5
5
  export declare const ContractAiFixturesDigest: "sha256:edac31c92e146ca1c40130f2fb5649db82e24b8cd1ac758153b90612052fddb6";
6
- export declare const ContractGeneratedGoDigest: "sha256:98bb6999738be926c36fd8d0735a17defdad590ced57e94bc431bb9bba53c9ad";
7
- export declare const ContractGeneratedTypeScriptDigest: "sha256:1d83b04e4899777f2963177e39e7ac99852cae80ffb8c03b6a294f5fb19d3600";
6
+ export declare const ContractGeneratedGoDigest: "sha256:8f3c3281a76792a9480ca1f7bcbad820db1908efffb29072fcdb0a8f99dbd208";
7
+ export declare const ContractGeneratedTypeScriptDigest: "sha256:82a083862e500a06df3550e694221cff9dcb98461d5174889c374713f961040c";
@@ -1,9 +1,9 @@
1
1
  /* eslint-disable */
2
2
  // Code generated by tools/gen/gen.mjs from schema/*.json. DO NOT EDIT.
3
- export const ContractRepositoryVersion = "0.51.0.0";
4
- export const ContractPublishVersion = "0.51.0";
5
- export const ContractPublishTag = "v0.51.0";
6
- export const ContractSchemasDigest = "sha256:018bf3078087a18b07990772c94669f658159bc84b0c8accedc7dd5daa38bee5";
3
+ export const ContractRepositoryVersion = "0.53.0.0";
4
+ export const ContractPublishVersion = "0.53.0";
5
+ export const ContractPublishTag = "v0.53.0";
6
+ export const ContractSchemasDigest = "sha256:c4d7c9642842bf3709003f447fb22429ce3573acabaa2da8279e0fc399e8d237";
7
7
  export const ContractAiFixturesDigest = "sha256:edac31c92e146ca1c40130f2fb5649db82e24b8cd1ac758153b90612052fddb6";
8
- export const ContractGeneratedGoDigest = "sha256:98bb6999738be926c36fd8d0735a17defdad590ced57e94bc431bb9bba53c9ad";
9
- export const ContractGeneratedTypeScriptDigest = "sha256:1d83b04e4899777f2963177e39e7ac99852cae80ffb8c03b6a294f5fb19d3600";
8
+ export const ContractGeneratedGoDigest = "sha256:8f3c3281a76792a9480ca1f7bcbad820db1908efffb29072fcdb0a8f99dbd208";
9
+ export const ContractGeneratedTypeScriptDigest = "sha256:82a083862e500a06df3550e694221cff9dcb98461d5174889c374713f961040c";
@@ -8,7 +8,7 @@ export type DataGrantV2Status = 'REQUESTED' | 'GRANTED' | 'DENIED' | 'REVOKED';
8
8
  export type DataGrantV2ProvisioningStatus = 'NONE' | 'PENDING' | 'FAILED' | 'DONE';
9
9
  export type DataGrantV2CleanupStatus = 'PENDING' | 'FAILED' | 'DONE';
10
10
  /**
11
- * An app's integration grant for one connection, environment and identity mode (control table sk APP#<appId>#IGRANT#<environment>#<sourceId>#<mode>; grantId = base64url(JSON ["v2",appId,environment,sourceId,mode])). Additive to the legacy DataGrant (data-grant.schema.json), which is never consulted by v2 paths. A fresh REQUESTED record carries empty `operations`/`resources` until the attached request is approved; approved resources are structured `{name, columns?}` entries (a Slack channel alias, the fixed Gmail `inbox` alias, or a warehouse view alias with its approved columns). Expiry is enforced from UTC time on every call and SDK calls never extend it; `expiredAt` is stamped by the recovery sweep afterwards. Status edges live in x-transitions; DENIED and REVOKED are terminal (a regrant after revocation is a new REQUESTED record once cleanup is DONE). Database grants add credentialRef/mintedPrincipal; both hold references only. `identityMode` and `installation` are the deprecated 0.45.0 spellings: the record carries `mode`, and a SaaS grant runs on the connection's single installation.
11
+ * An app's integration grant for one connection, environment and identity mode (control table sk APP#<appId>#IGRANT#<environment>#<sourceId>#<mode>; grantId = base64url(JSON ["v2",appId,environment,sourceId,mode])). Additive to the legacy DataGrant (data-grant.schema.json), which is never consulted by v2 paths. A fresh REQUESTED record carries empty `operations`/`resources` until the attached request is approved; approved resources are structured `{name, columns?}` entries (a Slack channel alias, the fixed Gmail `inbox` alias, or a warehouse view alias with its approved columns). Expiry is enforced from UTC time on every call and SDK calls never extend it; `expiredAt` is stamped by the recovery sweep afterwards. Status edges live in x-transitions; DENIED and REVOKED are terminal (a regrant after revocation is a new REQUESTED record once cleanup is DONE). Database grants add credentialRef/mintedPrincipal; both hold references only. An app-mode Slack grant may carry `presentation` (0.53.0): the IT-approved display name and icon the app's posts go out under, copied from the approved request and read by every post — never from the call. `identityMode` and `installation` are the deprecated 0.45.0 spellings: the record carries `mode`, and a SaaS grant runs on the connection's single installation.
12
12
  */
13
13
  export interface DataGrantV2 {
14
14
  schema: 'harbour.data-grant/2.0';
@@ -23,7 +23,7 @@ export interface DataGrantV2 {
23
23
  */
24
24
  sourceId: string;
25
25
  /**
26
- * Whose authority an operation runs under. Fixed per operation; callers never choose it.
26
+ * Whose authority an operation runs under. Fixed per operation (slack.message.post: declared per app); never chosen at call time.
27
27
  */
28
28
  mode: 'app' | 'user';
29
29
  kind: 'saas' | 'database';
@@ -58,6 +58,7 @@ export interface DataGrantV2 {
58
58
  * The one open access request attached to this grant, if any.
59
59
  */
60
60
  pendingRequestId?: string;
61
+ presentation?: IntegrationPresentation;
61
62
  provisioning: DataGrantV2Provisioning;
62
63
  /**
63
64
  * Database grants: secretref://tenant/{t}/app/{a}/env/{env}/source/{s}. Set only by a successful publish and removed by cleanup.
@@ -71,7 +72,7 @@ export interface DataGrantV2 {
71
72
  updatedAt: string;
72
73
  /**
73
74
  * @deprecated
74
- * Whose authority an operation runs under. Fixed per operation; callers never choose it.
75
+ * Whose authority an operation runs under. Fixed per operation (slack.message.post: declared per app); never chosen at call time.
75
76
  */
76
77
  identityMode?: 'app' | 'user';
77
78
  /**
@@ -96,6 +97,19 @@ export interface IntegrationResource {
96
97
  */
97
98
  columns?: string[];
98
99
  }
100
+ /**
101
+ * App-mode Slack grants only: how the app's posts are presented, as IT approved it. Absent means the posts carry the bot's own name.
102
+ */
103
+ interface IntegrationPresentation {
104
+ /**
105
+ * 1..80 characters of plain text: not blank, no control characters, no @-mentions (`@name`, `<@id>`). Must not impersonate a person: IT sees it before approving.
106
+ */
107
+ displayName: string;
108
+ /**
109
+ * A Slack emoji shortcode such as `:sandwich:`; absent keeps the bot's own icon.
110
+ */
111
+ iconEmoji?: string;
112
+ }
99
113
  export interface DataGrantV2Provisioning {
100
114
  status: DataGrantV2ProvisioningStatus;
101
115
  /**
@@ -126,3 +140,4 @@ export interface DataGrantV2Provisioning {
126
140
  export declare function canTransitionDataGrantV2Status(from: string, to: string): boolean;
127
141
  /** True iff the DataGrantV2Status is terminal (DENIED, REVOKED). */
128
142
  export declare function isTerminalDataGrantV2Status(state: string): boolean;
143
+ export {};
@@ -1,5 +1,5 @@
1
1
  /**
2
- * 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}. 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.
2
+ * 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 (declared per app for slack.message.post) and enforced from the 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 Harbour" footer) or as the signed-in person (no name, no footer), whichever mode the app declared and IT approved — the same input either way. 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.
3
3
  */
4
4
  export type IntegrationExecuteRequest = {
5
5
  /**
@@ -7,7 +7,7 @@ export type IntegrationExecuteRequest = {
7
7
  */
8
8
  connection: string;
9
9
  /**
10
- * Closed operation set. Extending it is a contracts change. gmail.message.send (0.50.0) sends one plain-text email as the signed-in person: a send like slack.message.post (explicit Send, idempotencyKey, write receipt) under user identity.
10
+ * Closed operation set. Extending it is a contracts change. gmail.message.send (0.50.0) sends one plain-text email as the signed-in person: a send like slack.message.post (explicit Send, idempotencyKey, write receipt) under user identity. slack.message.post (0.53.0) runs in the mode the app declares: app (the company's Slack app, under the IT-approved per-app name) or user (the signed-in person, after their consent).
11
11
  */
12
12
  operation: 'slack.channel.history' | 'slack.message.post' | 'gmail.thread.list' | 'gmail.message.read' | 'gmail.message.send' | 'warehouse.view.read';
13
13
  /**
@@ -93,6 +93,12 @@ export interface SaasInstallation {
93
93
  botUserId: string;
94
94
  status: SaasInstallationStatus;
95
95
  installedAt: string;
96
+ /**
97
+ * The bot scopes Slack granted at installation (0.53.0). Absent on installations recorded before it, which therefore lack chat:write.customize: per-app display names need a reinstall.
98
+ *
99
+ * @maxItems 64
100
+ */
101
+ scopes?: string[];
96
102
  }
97
103
  export interface DevelopmentPreapproval {
98
104
  /**
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Structured output an agent must return from one Harbour transformation attempt. Handed to the agent as its output schema, then decoded and semantically re-validated by the control plane, which remains the authority. The enum on applied_playbooks.items is the one dynamic part: the adapter injects the names of the playbooks supplied for that invocation before writing the document, so the model is structurally prevented from naming a playbook it was not given. Application-secret handling was removed platform-wide, so this document carries no redactions and no secrets fields. A deployable report must also carry preservation_plan: the agent's own grounded smoke tests (one to three happy_path user journeys plus a boundary_check per behaviour its playbooks or edits could regress) that the validation cell executes after transformation, and an uncovered list naming every boundary it could not test and why. It also carries validation_fixtures: null when no external fixture is needed, or a bounded generic HTTP/WebSocket fixture declaration for that one run. An unsupported report carries only application.unsupported and no plan or fixtures.
2
+ * Structured output an agent must return from one Harbour transformation attempt. Handed to the agent as its output schema, then decoded and semantically re-validated by the control plane, which remains the authority. The enum on applied_playbooks.items is the one dynamic part: the adapter injects the names of the playbooks supplied for that invocation before writing the document, so the model is structurally prevented from naming a playbook it was not given. Application-secret handling was removed platform-wide, so this document carries no redactions and no secrets fields. A deployable report must also carry preservation_plan: the agent's own grounded smoke tests (one to three happy_path user journeys plus a boundary_check per behaviour its playbooks or edits could regress) that the validation cell executes after transformation, and an uncovered list naming every boundary it could not test and why. It may also carry validation_fixtures: null when no external fixture is needed, or a bounded generic HTTP/WebSocket fixture declaration for that one run; the deployment plane reads the workspace fixture sidecar, so the report's copy is optional. An unsupported report carries only application.unsupported and no plan or fixtures.
3
3
  */
4
4
  export type TransformReport = {
5
5
  /**
@@ -8,23 +8,33 @@ export type TransformReport = {
8
8
  * @minItems 1
9
9
  */
10
10
  applied_playbooks?: string[];
11
+ /**
12
+ * Optional. The deployed value is a platform constant supplied to the build, not a report field, so a deployable report may omit it and one that still carries it stays valid.
13
+ */
11
14
  static_rebuild_required?: boolean;
12
15
  analysis?: {
13
16
  deployable: boolean;
14
- needsTransformation: boolean;
17
+ /**
18
+ * Whether the attempt changed the tree. Optional: Harbour derives this from the transformed workspace, so a deployable report may omit it and one that still carries it stays valid.
19
+ */
20
+ needsTransformation?: boolean;
15
21
  /**
16
22
  * @maxItems 64
17
23
  */
18
24
  blockers: string[];
19
25
  /**
26
+ * Paths the attempt touched. Optional: Harbour derives this from the transformed workspace, so a deployable report may omit it and one that still carries it stays valid.
27
+ *
20
28
  * @minItems 1
21
29
  * @maxItems 1024
22
30
  */
23
- surfaces: string[];
31
+ surfaces?: string[];
24
32
  /**
33
+ * Optional file-and-line citations for why the transformation was needed. Nothing downstream reads them, so a deployable report may omit them and one that still carries them stays valid.
34
+ *
25
35
  * @maxItems 1024
26
36
  */
27
- evidence: {
37
+ evidence?: {
28
38
  path: string;
29
39
  line: number;
30
40
  reason: string;
@@ -32,29 +42,46 @@ export type TransformReport = {
32
42
  };
33
43
  database?: {
34
44
  mode: 'none' | 'sql';
35
- changeType: 'none' | 'code_only' | 'schema_only' | 'code_and_schema';
36
- requiresCodeChanges: boolean;
37
- requiresSchemaMigration: boolean;
38
45
  /**
46
+ * Classification of the changed paths. Optional: Harbour derives this from the transformed workspace, so a deployable report may omit it and one that still carries it stays valid.
47
+ */
48
+ changeType?: 'none' | 'code_only' | 'schema_only' | 'code_and_schema';
49
+ /**
50
+ * Whether any changed path sits outside the database boundary. Optional: Harbour derives this from the transformed workspace, so a deployable report may omit it and one that still carries it stays valid.
51
+ */
52
+ requiresCodeChanges?: boolean;
53
+ /**
54
+ * Whether the Aurora bundle grew past its seeded prefix. Optional: Harbour derives this from the transformed workspace, so a deployable report may omit it and one that still carries it stays valid.
55
+ */
56
+ requiresSchemaMigration?: boolean;
57
+ /**
58
+ * Migration files in the Aurora bundle. Optional: Harbour derives this from the transformed workspace, so a deployable report may omit it and one that still carries it stays valid.
59
+ *
39
60
  * @maxItems 128
40
61
  */
41
- migrationPaths: string[];
62
+ migrationPaths?: string[];
42
63
  /**
64
+ * Changed paths on the database boundary. Optional: Harbour derives this from the transformed workspace, so a deployable report may omit it and one that still carries it stays valid.
65
+ *
43
66
  * @maxItems 512
44
67
  */
45
- changedFiles: string[];
68
+ changedFiles?: string[];
46
69
  /**
70
+ * Optional file-and-line citations for the database findings. Nothing downstream reads them, so a deployable report may omit them and one that still carries them stays valid.
71
+ *
47
72
  * @maxItems 128
48
73
  */
49
- evidence: {
74
+ evidence?: {
50
75
  path: string;
51
76
  line: number;
52
77
  reason: string;
53
78
  }[];
54
79
  /**
80
+ * Optional. The platform's only structural decline is application.unsupported; nothing reads this list, so a deployable report may omit it and one that still carries it stays valid.
81
+ *
55
82
  * @maxItems 64
56
83
  */
57
- unsupportedFeatures: string[];
84
+ unsupportedFeatures?: string[];
58
85
  };
59
86
  /**
60
87
  * Agent-authored preservation (smoke) plan for this attempt. Required whenever the report is deployable; forbidden on an unsupported report. Every test must be grounded in files the agent actually read or changed (evidence) and expressed as tenant-relative browser or HTTP steps the validation cell can execute without secrets. Structural bounds live here; per-action required fields, at least one assert* step per test, one to three happy_path tests, unique ids, the {{runId}} substitution and path safety are enforced semantically by the deployment plane.
@@ -72,12 +99,12 @@ export type TransformReport = {
72
99
  kind: 'happy_path' | 'boundary_check';
73
100
  name: string;
74
101
  /**
75
- * Transformation boundaries this test claims to cover.
102
+ * Transformation boundaries this test claims to cover. Optional: Harbour derives them from the test's own evidence paths, so a deployable report may omit them and one that still carries them stays valid.
76
103
  *
77
104
  * @minItems 1
78
105
  * @maxItems 8
79
106
  */
80
- boundaries: ('application' | 'routing' | 'database' | 'authentication' | 'runtime_dependency' | 'files' | 'provider' | 'background_work')[];
107
+ boundaries?: ('application' | 'routing' | 'database' | 'authentication' | 'runtime_dependency' | 'files' | 'provider' | 'background_work')[];
81
108
  /**
82
109
  * Files (and optionally lines) in the transformed workspace that ground this test. Paths must exist after transformation.
83
110
  *
@@ -147,7 +174,10 @@ export type TransformReport = {
147
174
  * @maxItems 8
148
175
  */
149
176
  uncovered: {
150
- boundary: 'application' | 'routing' | 'database' | 'authentication' | 'runtime_dependency' | 'files' | 'provider' | 'background_work';
177
+ /**
178
+ * Optional: Harbour derives the uncovered boundaries from the changed paths and the covered set, so a deployable report may omit this and one that still carries it stays valid.
179
+ */
180
+ boundary?: 'application' | 'routing' | 'database' | 'authentication' | 'runtime_dependency' | 'files' | 'provider' | 'background_work';
151
181
  reason: 'not-observable-in-validation-cell' | 'no-safe-fixture' | 'no-grounded-surface';
152
182
  detail?: string;
153
183
  }[];
@@ -224,7 +254,13 @@ export type TransformReport = {
224
254
  kind: 'sql';
225
255
  path: string;
226
256
  } | null;
257
+ /**
258
+ * Optional: determined by topology (static topology iff static build), so a deployable report may omit it and one that still carries it stays valid.
259
+ */
227
260
  buildKind?: 'container' | 'static';
261
+ /**
262
+ * Optional: determined by the build output kinds, so a deployable report may omit it and one that still carries it stays valid.
263
+ */
228
264
  topology?: 'static' | 'backend' | 'frontend_and_backend';
229
265
  entrypoint?: string;
230
266
  build?: {
@@ -248,6 +284,9 @@ export type TransformReport = {
248
284
  route: string;
249
285
  }[];
250
286
  };
287
+ /**
288
+ * Optional: both members are platform constants, so a deployable report may omit this object and one that still carries it stays valid.
289
+ */
251
290
  basePath?: {
252
291
  required: boolean;
253
292
  environmentVariable: 'HARBOUR_BASE_PATH';
@@ -262,6 +301,8 @@ export type TransformReport = {
262
301
  reason: string;
263
302
  }[];
264
303
  /**
304
+ * Optional: a deployable plan requires this empty, so a deployable report may omit it and one that still carries it (empty) stays valid.
305
+ *
265
306
  * @maxItems 64
266
307
  */
267
308
  unsupportedFeatures?: string[];
@@ -11,13 +11,17 @@ export type IntegrationAlias = string;
11
11
  * @maxItems 64
12
12
  */
13
13
  export type ResourceNames = IntegrationAlias[];
14
+ /**
15
+ * Whose authority an operation runs under. Fixed per operation (slack.message.post: declared per app); never chosen at call time.
16
+ */
17
+ export type IntegrationIdentityMode = 'app' | 'user';
14
18
  /**
15
19
  * A plain column identifier of a warehouse view.
16
20
  */
17
21
  export type ColumnName = string;
18
22
 
19
23
  /**
20
- * Committed `.harbour/integrations.json`: the app's declared use of tenant-wide integration connections. Keys are logical connection aliases (the governance sourceId); resources are logical names (Slack channel alias, warehouse view name), never IDs, tokens or URLs. The operation set is closed (five operations) and each operation carries a fixed identity mode: slack.channel.history=user, slack.message.post=app, gmail.thread.list=user, gmail.message.read=user, warehouse.view.read=app. A `saas` connection may declare only the Slack and Gmail operations and a `database` connection only warehouse.view.read. Gmail is read-only and user-mode only; its single resource is the fixed `inbox` alias. This file also owns the shared IntegrationOperation / IntegrationIdentityMode / IntegrationEnvironment / IntegrationConnectionKind enums referenced by the other integration schemas.
24
+ * Committed `.harbour/integrations.json`: the app's declared use of tenant-wide integration connections. Keys are logical connection aliases (the governance sourceId); resources are logical names (Slack channel alias, warehouse view name), never IDs, tokens or URLs. The operation set is closed (six operations). Each operation carries a fixed identity mode — slack.channel.history=user, gmail.thread.list=user, gmail.message.read=user, gmail.message.send=user, warehouse.view.read=app — except slack.message.post, whose mode the app DECLARES: `app` posts as the company's Slack app (the one audited bot identity, under the IT-approved per-app display name when the connection declares `presentation`), `user` posts as the signed-in person after their own consent. The mode is never chosen at call time: governance runs the call under the approved grant of the declared mode. A `saas` connection may declare only the Slack and Gmail operations and a `database` connection only warehouse.view.read; `presentation` is a `saas` connection's optional per-app display name and icon for app-mode Slack posts, shown to IT at approval time and applied only from the approved grant. Gmail is user-mode only (reads plus one send); its single resource is the fixed `inbox` alias. This file also owns the shared IntegrationOperation / IntegrationIdentityMode / IntegrationEnvironment / IntegrationConnectionKind / IntegrationPresentation definitions referenced by the other integration schemas.
21
25
  */
22
26
  export interface AppIntegrationsDeclaration {
23
27
  schema: 'harbour.app-integrations/2.0';
@@ -27,21 +31,38 @@ export interface AppIntegrationsDeclaration {
27
31
  }
28
32
  export interface DeclaredConnection {
29
33
  kind: IntegrationConnectionKind;
34
+ presentation?: IntegrationPresentation;
30
35
  operations: {
31
36
  'slack.channel.history'?: UserModeChannelOperation;
32
- 'slack.message.post'?: AppModeChannelOperation;
37
+ 'slack.message.post'?: DeclaredModeChannelOperation;
33
38
  'gmail.thread.list'?: UserModeChannelOperation;
34
39
  'gmail.message.read'?: UserModeChannelOperation;
35
40
  'gmail.message.send'?: UserModeChannelOperation;
36
41
  'warehouse.view.read'?: AppModeViewOperation;
37
42
  };
38
43
  }
44
+ /**
45
+ * saas connections only (Slack): how this app's app-mode posts are presented; meaningless for every other operation.
46
+ */
47
+ export interface IntegrationPresentation {
48
+ /**
49
+ * 1..80 characters of plain text: not blank, no control characters, no @-mentions (`@name`, `<@id>`). Must not impersonate a person: IT sees it before approving.
50
+ */
51
+ displayName: string;
52
+ /**
53
+ * A Slack emoji shortcode such as `:sandwich:`; absent keeps the bot's own icon.
54
+ */
55
+ iconEmoji?: string;
56
+ }
39
57
  export interface UserModeChannelOperation {
40
58
  identity: 'user';
41
59
  resources: ResourceNames;
42
60
  }
43
- export interface AppModeChannelOperation {
44
- identity: 'app';
61
+ /**
62
+ * slack.message.post: the app declares the mode — app (posts as the company's Slack app) or user (posts as the signed-in person).
63
+ */
64
+ export interface DeclaredModeChannelOperation {
65
+ identity: IntegrationIdentityMode;
45
66
  resources: ResourceNames;
46
67
  }
47
68
  export interface AppModeViewOperation {
@@ -1,10 +1,10 @@
1
1
  /* eslint-disable */
2
2
  // Code generated by tools/gen/gen.mjs from schema/*.json. DO NOT EDIT.
3
3
 
4
- export const ContractRepositoryVersion = "0.51.0.0" as const;
5
- export const ContractPublishVersion = "0.51.0" as const;
6
- export const ContractPublishTag = "v0.51.0" as const;
7
- export const ContractSchemasDigest = "sha256:018bf3078087a18b07990772c94669f658159bc84b0c8accedc7dd5daa38bee5" as const;
4
+ export const ContractRepositoryVersion = "0.53.0.0" as const;
5
+ export const ContractPublishVersion = "0.53.0" as const;
6
+ export const ContractPublishTag = "v0.53.0" as const;
7
+ export const ContractSchemasDigest = "sha256:c4d7c9642842bf3709003f447fb22429ce3573acabaa2da8279e0fc399e8d237" as const;
8
8
  export const ContractAiFixturesDigest = "sha256:edac31c92e146ca1c40130f2fb5649db82e24b8cd1ac758153b90612052fddb6" as const;
9
- export const ContractGeneratedGoDigest = "sha256:98bb6999738be926c36fd8d0735a17defdad590ced57e94bc431bb9bba53c9ad" as const;
10
- export const ContractGeneratedTypeScriptDigest = "sha256:1d83b04e4899777f2963177e39e7ac99852cae80ffb8c03b6a294f5fb19d3600" as const;
9
+ export const ContractGeneratedGoDigest = "sha256:8f3c3281a76792a9480ca1f7bcbad820db1908efffb29072fcdb0a8f99dbd208" as const;
10
+ export const ContractGeneratedTypeScriptDigest = "sha256:82a083862e500a06df3550e694221cff9dcb98461d5174889c374713f961040c" as const;
@@ -12,7 +12,7 @@ export type DataGrantV2ProvisioningStatus = 'NONE' | 'PENDING' | 'FAILED' | 'DON
12
12
  export type DataGrantV2CleanupStatus = 'PENDING' | 'FAILED' | 'DONE';
13
13
 
14
14
  /**
15
- * An app's integration grant for one connection, environment and identity mode (control table sk APP#<appId>#IGRANT#<environment>#<sourceId>#<mode>; grantId = base64url(JSON ["v2",appId,environment,sourceId,mode])). Additive to the legacy DataGrant (data-grant.schema.json), which is never consulted by v2 paths. A fresh REQUESTED record carries empty `operations`/`resources` until the attached request is approved; approved resources are structured `{name, columns?}` entries (a Slack channel alias, the fixed Gmail `inbox` alias, or a warehouse view alias with its approved columns). Expiry is enforced from UTC time on every call and SDK calls never extend it; `expiredAt` is stamped by the recovery sweep afterwards. Status edges live in x-transitions; DENIED and REVOKED are terminal (a regrant after revocation is a new REQUESTED record once cleanup is DONE). Database grants add credentialRef/mintedPrincipal; both hold references only. `identityMode` and `installation` are the deprecated 0.45.0 spellings: the record carries `mode`, and a SaaS grant runs on the connection's single installation.
15
+ * An app's integration grant for one connection, environment and identity mode (control table sk APP#<appId>#IGRANT#<environment>#<sourceId>#<mode>; grantId = base64url(JSON ["v2",appId,environment,sourceId,mode])). Additive to the legacy DataGrant (data-grant.schema.json), which is never consulted by v2 paths. A fresh REQUESTED record carries empty `operations`/`resources` until the attached request is approved; approved resources are structured `{name, columns?}` entries (a Slack channel alias, the fixed Gmail `inbox` alias, or a warehouse view alias with its approved columns). Expiry is enforced from UTC time on every call and SDK calls never extend it; `expiredAt` is stamped by the recovery sweep afterwards. Status edges live in x-transitions; DENIED and REVOKED are terminal (a regrant after revocation is a new REQUESTED record once cleanup is DONE). Database grants add credentialRef/mintedPrincipal; both hold references only. An app-mode Slack grant may carry `presentation` (0.53.0): the IT-approved display name and icon the app's posts go out under, copied from the approved request and read by every post — never from the call. `identityMode` and `installation` are the deprecated 0.45.0 spellings: the record carries `mode`, and a SaaS grant runs on the connection's single installation.
16
16
  */
17
17
  export interface DataGrantV2 {
18
18
  schema: 'harbour.data-grant/2.0';
@@ -27,7 +27,7 @@ export interface DataGrantV2 {
27
27
  */
28
28
  sourceId: string;
29
29
  /**
30
- * Whose authority an operation runs under. Fixed per operation; callers never choose it.
30
+ * Whose authority an operation runs under. Fixed per operation (slack.message.post: declared per app); never chosen at call time.
31
31
  */
32
32
  mode: 'app' | 'user';
33
33
  kind: 'saas' | 'database';
@@ -69,6 +69,7 @@ export interface DataGrantV2 {
69
69
  * The one open access request attached to this grant, if any.
70
70
  */
71
71
  pendingRequestId?: string;
72
+ presentation?: IntegrationPresentation;
72
73
  provisioning: DataGrantV2Provisioning;
73
74
  /**
74
75
  * Database grants: secretref://tenant/{t}/app/{a}/env/{env}/source/{s}. Set only by a successful publish and removed by cleanup.
@@ -82,7 +83,7 @@ export interface DataGrantV2 {
82
83
  updatedAt: string;
83
84
  /**
84
85
  * @deprecated
85
- * Whose authority an operation runs under. Fixed per operation; callers never choose it.
86
+ * Whose authority an operation runs under. Fixed per operation (slack.message.post: declared per app); never chosen at call time.
86
87
  */
87
88
  identityMode?: 'app' | 'user';
88
89
  /**
@@ -107,6 +108,19 @@ export interface IntegrationResource {
107
108
  */
108
109
  columns?: string[];
109
110
  }
111
+ /**
112
+ * App-mode Slack grants only: how the app's posts are presented, as IT approved it. Absent means the posts carry the bot's own name.
113
+ */
114
+ interface IntegrationPresentation {
115
+ /**
116
+ * 1..80 characters of plain text: not blank, no control characters, no @-mentions (`@name`, `<@id>`). Must not impersonate a person: IT sees it before approving.
117
+ */
118
+ displayName: string;
119
+ /**
120
+ * A Slack emoji shortcode such as `:sandwich:`; absent keeps the bot's own icon.
121
+ */
122
+ iconEmoji?: string;
123
+ }
110
124
  export interface DataGrantV2Provisioning {
111
125
  status: DataGrantV2ProvisioningStatus;
112
126
  /**
@@ -2,7 +2,7 @@
2
2
  // Code generated by tools/gen/gen.mjs from schema/*.json. DO NOT EDIT.
3
3
 
4
4
  /**
5
- * 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}. 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
+ * 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 (declared per app for slack.message.post) and enforced from the 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 Harbour" footer) or as the signed-in person (no name, no footer), whichever mode the app declared and IT approved — the same input either way. 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.
6
6
  */
7
7
  export type IntegrationExecuteRequest = {
8
8
  /**
@@ -10,7 +10,7 @@ export type IntegrationExecuteRequest = {
10
10
  */
11
11
  connection: string;
12
12
  /**
13
- * Closed operation set. Extending it is a contracts change. gmail.message.send (0.50.0) sends one plain-text email as the signed-in person: a send like slack.message.post (explicit Send, idempotencyKey, write receipt) under user identity.
13
+ * Closed operation set. Extending it is a contracts change. gmail.message.send (0.50.0) sends one plain-text email as the signed-in person: a send like slack.message.post (explicit Send, idempotencyKey, write receipt) under user identity. slack.message.post (0.53.0) runs in the mode the app declares: app (the company's Slack app, under the IT-approved per-app name) or user (the signed-in person, after their consent).
14
14
  */
15
15
  operation:
16
16
  | 'slack.channel.history'
@@ -104,6 +104,12 @@ export interface SaasInstallation {
104
104
  botUserId: string;
105
105
  status: SaasInstallationStatus;
106
106
  installedAt: string;
107
+ /**
108
+ * The bot scopes Slack granted at installation (0.53.0). Absent on installations recorded before it, which therefore lack chat:write.customize: per-app display names need a reinstall.
109
+ *
110
+ * @maxItems 64
111
+ */
112
+ scopes?: string[];
107
113
  }
108
114
  export interface DevelopmentPreapproval {
109
115
  /**
@@ -2,7 +2,7 @@
2
2
  // Code generated by tools/gen/gen.mjs from schema/*.json. DO NOT EDIT.
3
3
 
4
4
  /**
5
- * Structured output an agent must return from one Harbour transformation attempt. Handed to the agent as its output schema, then decoded and semantically re-validated by the control plane, which remains the authority. The enum on applied_playbooks.items is the one dynamic part: the adapter injects the names of the playbooks supplied for that invocation before writing the document, so the model is structurally prevented from naming a playbook it was not given. Application-secret handling was removed platform-wide, so this document carries no redactions and no secrets fields. A deployable report must also carry preservation_plan: the agent's own grounded smoke tests (one to three happy_path user journeys plus a boundary_check per behaviour its playbooks or edits could regress) that the validation cell executes after transformation, and an uncovered list naming every boundary it could not test and why. It also carries validation_fixtures: null when no external fixture is needed, or a bounded generic HTTP/WebSocket fixture declaration for that one run. An unsupported report carries only application.unsupported and no plan or fixtures.
5
+ * Structured output an agent must return from one Harbour transformation attempt. Handed to the agent as its output schema, then decoded and semantically re-validated by the control plane, which remains the authority. The enum on applied_playbooks.items is the one dynamic part: the adapter injects the names of the playbooks supplied for that invocation before writing the document, so the model is structurally prevented from naming a playbook it was not given. Application-secret handling was removed platform-wide, so this document carries no redactions and no secrets fields. A deployable report must also carry preservation_plan: the agent's own grounded smoke tests (one to three happy_path user journeys plus a boundary_check per behaviour its playbooks or edits could regress) that the validation cell executes after transformation, and an uncovered list naming every boundary it could not test and why. It may also carry validation_fixtures: null when no external fixture is needed, or a bounded generic HTTP/WebSocket fixture declaration for that one run; the deployment plane reads the workspace fixture sidecar, so the report's copy is optional. An unsupported report carries only application.unsupported and no plan or fixtures.
6
6
  */
7
7
  export type TransformReport = {
8
8
  /**
@@ -11,23 +11,33 @@ export type TransformReport = {
11
11
  * @minItems 1
12
12
  */
13
13
  applied_playbooks?: string[];
14
+ /**
15
+ * Optional. The deployed value is a platform constant supplied to the build, not a report field, so a deployable report may omit it and one that still carries it stays valid.
16
+ */
14
17
  static_rebuild_required?: boolean;
15
18
  analysis?: {
16
19
  deployable: boolean;
17
- needsTransformation: boolean;
20
+ /**
21
+ * Whether the attempt changed the tree. Optional: Harbour derives this from the transformed workspace, so a deployable report may omit it and one that still carries it stays valid.
22
+ */
23
+ needsTransformation?: boolean;
18
24
  /**
19
25
  * @maxItems 64
20
26
  */
21
27
  blockers: string[];
22
28
  /**
29
+ * Paths the attempt touched. Optional: Harbour derives this from the transformed workspace, so a deployable report may omit it and one that still carries it stays valid.
30
+ *
23
31
  * @minItems 1
24
32
  * @maxItems 1024
25
33
  */
26
- surfaces: string[];
34
+ surfaces?: string[];
27
35
  /**
36
+ * Optional file-and-line citations for why the transformation was needed. Nothing downstream reads them, so a deployable report may omit them and one that still carries them stays valid.
37
+ *
28
38
  * @maxItems 1024
29
39
  */
30
- evidence: {
40
+ evidence?: {
31
41
  path: string;
32
42
  line: number;
33
43
  reason: string;
@@ -35,29 +45,46 @@ export type TransformReport = {
35
45
  };
36
46
  database?: {
37
47
  mode: 'none' | 'sql';
38
- changeType: 'none' | 'code_only' | 'schema_only' | 'code_and_schema';
39
- requiresCodeChanges: boolean;
40
- requiresSchemaMigration: boolean;
41
48
  /**
49
+ * Classification of the changed paths. Optional: Harbour derives this from the transformed workspace, so a deployable report may omit it and one that still carries it stays valid.
50
+ */
51
+ changeType?: 'none' | 'code_only' | 'schema_only' | 'code_and_schema';
52
+ /**
53
+ * Whether any changed path sits outside the database boundary. Optional: Harbour derives this from the transformed workspace, so a deployable report may omit it and one that still carries it stays valid.
54
+ */
55
+ requiresCodeChanges?: boolean;
56
+ /**
57
+ * Whether the Aurora bundle grew past its seeded prefix. Optional: Harbour derives this from the transformed workspace, so a deployable report may omit it and one that still carries it stays valid.
58
+ */
59
+ requiresSchemaMigration?: boolean;
60
+ /**
61
+ * Migration files in the Aurora bundle. Optional: Harbour derives this from the transformed workspace, so a deployable report may omit it and one that still carries it stays valid.
62
+ *
42
63
  * @maxItems 128
43
64
  */
44
- migrationPaths: string[];
65
+ migrationPaths?: string[];
45
66
  /**
67
+ * Changed paths on the database boundary. Optional: Harbour derives this from the transformed workspace, so a deployable report may omit it and one that still carries it stays valid.
68
+ *
46
69
  * @maxItems 512
47
70
  */
48
- changedFiles: string[];
71
+ changedFiles?: string[];
49
72
  /**
73
+ * Optional file-and-line citations for the database findings. Nothing downstream reads them, so a deployable report may omit them and one that still carries them stays valid.
74
+ *
50
75
  * @maxItems 128
51
76
  */
52
- evidence: {
77
+ evidence?: {
53
78
  path: string;
54
79
  line: number;
55
80
  reason: string;
56
81
  }[];
57
82
  /**
83
+ * Optional. The platform's only structural decline is application.unsupported; nothing reads this list, so a deployable report may omit it and one that still carries it stays valid.
84
+ *
58
85
  * @maxItems 64
59
86
  */
60
- unsupportedFeatures: string[];
87
+ unsupportedFeatures?: string[];
61
88
  };
62
89
  /**
63
90
  * Agent-authored preservation (smoke) plan for this attempt. Required whenever the report is deployable; forbidden on an unsupported report. Every test must be grounded in files the agent actually read or changed (evidence) and expressed as tenant-relative browser or HTTP steps the validation cell can execute without secrets. Structural bounds live here; per-action required fields, at least one assert* step per test, one to three happy_path tests, unique ids, the {{runId}} substitution and path safety are enforced semantically by the deployment plane.
@@ -75,12 +102,12 @@ export type TransformReport = {
75
102
  kind: 'happy_path' | 'boundary_check';
76
103
  name: string;
77
104
  /**
78
- * Transformation boundaries this test claims to cover.
105
+ * Transformation boundaries this test claims to cover. Optional: Harbour derives them from the test's own evidence paths, so a deployable report may omit them and one that still carries them stays valid.
79
106
  *
80
107
  * @minItems 1
81
108
  * @maxItems 8
82
109
  */
83
- boundaries: (
110
+ boundaries?: (
84
111
  | 'application'
85
112
  | 'routing'
86
113
  | 'database'
@@ -159,7 +186,10 @@ export type TransformReport = {
159
186
  * @maxItems 8
160
187
  */
161
188
  uncovered: {
162
- boundary:
189
+ /**
190
+ * Optional: Harbour derives the uncovered boundaries from the changed paths and the covered set, so a deployable report may omit this and one that still carries it stays valid.
191
+ */
192
+ boundary?:
163
193
  | 'application'
164
194
  | 'routing'
165
195
  | 'database'
@@ -247,7 +277,13 @@ export type TransformReport = {
247
277
  kind: 'sql';
248
278
  path: string;
249
279
  } | null;
280
+ /**
281
+ * Optional: determined by topology (static topology iff static build), so a deployable report may omit it and one that still carries it stays valid.
282
+ */
250
283
  buildKind?: 'container' | 'static';
284
+ /**
285
+ * Optional: determined by the build output kinds, so a deployable report may omit it and one that still carries it stays valid.
286
+ */
251
287
  topology?: 'static' | 'backend' | 'frontend_and_backend';
252
288
  entrypoint?: string;
253
289
  build?: {
@@ -271,6 +307,9 @@ export type TransformReport = {
271
307
  route: string;
272
308
  }[];
273
309
  };
310
+ /**
311
+ * Optional: both members are platform constants, so a deployable report may omit this object and one that still carries it stays valid.
312
+ */
274
313
  basePath?: {
275
314
  required: boolean;
276
315
  environmentVariable: 'HARBOUR_BASE_PATH';
@@ -285,6 +324,8 @@ export type TransformReport = {
285
324
  reason: string;
286
325
  }[];
287
326
  /**
327
+ * Optional: a deployable plan requires this empty, so a deployable report may omit it and one that still carries it (empty) stays valid.
328
+ *
288
329
  * @maxItems 64
289
330
  */
290
331
  unsupportedFeatures?: string[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fourier-labs/harbour-contracts",
3
- "version": "0.51.0",
3
+ "version": "0.53.0",
4
4
  "description": "Single source of truth for every data shape that crosses a boundary between Harbour components.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -2,7 +2,7 @@
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
3
  "$id": "https://schemas.harbour.dev/app-integrations-declaration.schema.json",
4
4
  "title": "AppIntegrationsDeclaration",
5
- "description": "Committed `.harbour/integrations.json`: the app's declared use of tenant-wide integration connections. Keys are logical connection aliases (the governance sourceId); resources are logical names (Slack channel alias, warehouse view name), never IDs, tokens or URLs. The operation set is closed (five operations) and each operation carries a fixed identity mode: slack.channel.history=user, slack.message.post=app, gmail.thread.list=user, gmail.message.read=user, warehouse.view.read=app. A `saas` connection may declare only the Slack and Gmail operations and a `database` connection only warehouse.view.read. Gmail is read-only and user-mode only; its single resource is the fixed `inbox` alias. This file also owns the shared IntegrationOperation / IntegrationIdentityMode / IntegrationEnvironment / IntegrationConnectionKind enums referenced by the other integration schemas.",
5
+ "description": "Committed `.harbour/integrations.json`: the app's declared use of tenant-wide integration connections. Keys are logical connection aliases (the governance sourceId); resources are logical names (Slack channel alias, warehouse view name), never IDs, tokens or URLs. The operation set is closed (six operations). Each operation carries a fixed identity mode — slack.channel.history=user, gmail.thread.list=user, gmail.message.read=user, gmail.message.send=user, warehouse.view.read=app — except slack.message.post, whose mode the app DECLARES: `app` posts as the company's Slack app (the one audited bot identity, under the IT-approved per-app display name when the connection declares `presentation`), `user` posts as the signed-in person after their own consent. The mode is never chosen at call time: governance runs the call under the approved grant of the declared mode. A `saas` connection may declare only the Slack and Gmail operations and a `database` connection only warehouse.view.read; `presentation` is a `saas` connection's optional per-app display name and icon for app-mode Slack posts, shown to IT at approval time and applied only from the approved grant. Gmail is user-mode only (reads plus one send); its single resource is the fixed `inbox` alias. This file also owns the shared IntegrationOperation / IntegrationIdentityMode / IntegrationEnvironment / IntegrationConnectionKind / IntegrationPresentation definitions referenced by the other integration schemas.",
6
6
  "type": "object",
7
7
  "additionalProperties": false,
8
8
  "required": ["schema", "connections"],
@@ -26,18 +26,34 @@
26
26
  "IntegrationOperation": {
27
27
  "type": "string",
28
28
  "enum": ["slack.channel.history", "slack.message.post", "gmail.thread.list", "gmail.message.read", "gmail.message.send", "warehouse.view.read"],
29
- "description": "Closed operation set. Extending it is a contracts change. gmail.message.send (0.50.0) sends one plain-text email as the signed-in person: a send like slack.message.post (explicit Send, idempotencyKey, write receipt) under user identity."
29
+ "description": "Closed operation set. Extending it is a contracts change. gmail.message.send (0.50.0) sends one plain-text email as the signed-in person: a send like slack.message.post (explicit Send, idempotencyKey, write receipt) under user identity. slack.message.post (0.53.0) runs in the mode the app declares: app (the company's Slack app, under the IT-approved per-app name) or user (the signed-in person, after their consent)."
30
30
  },
31
31
  "IntegrationIdentityMode": {
32
32
  "type": "string",
33
33
  "enum": ["app", "user"],
34
- "description": "Whose authority an operation runs under. Fixed per operation; callers never choose it."
34
+ "description": "Whose authority an operation runs under. Fixed per operation (slack.message.post: declared per app); never chosen at call time."
35
35
  },
36
36
  "IntegrationEnvironment": {
37
37
  "type": "string",
38
38
  "enum": ["development", "preview", "production"],
39
39
  "description": "development is the CLI/local kit; preview and production are hosted."
40
40
  },
41
+ "IntegrationPresentation": {
42
+ "type": "object",
43
+ "additionalProperties": false,
44
+ "required": ["displayName"],
45
+ "description": "How an app's app-mode Slack posts are presented (0.53.0): the display name and optional icon the platform bot posts under, so each app's posts are distinguishable while the bot stays the one audited identity. Declared by the app, shown to IT on the access request (\"posts as\"), copied onto the approved grant and applied only from there — a call can never name itself. Needs the Slack app scope chat:write.customize on the tenant's installation; without it the post goes out under the bot's own name and the receipt says so.",
46
+ "properties": {
47
+ "displayName": {
48
+ "type": "string",
49
+ "minLength": 1,
50
+ "maxLength": 80,
51
+ "pattern": "^(?!\\s*$)(?!.*(^|[\\s(\\[<\"'])@[A-Za-z0-9_])(?!.*<@)[^\\u0000-\\u001F\\u007F]*$",
52
+ "description": "1..80 characters of plain text: not blank, no control characters, no @-mentions (`@name`, `<@id>`). Must not impersonate a person: IT sees it before approving."
53
+ },
54
+ "iconEmoji": { "type": "string", "pattern": "^:[a-z0-9_+-]{1,60}:$", "description": "A Slack emoji shortcode such as `:sandwich:`; absent keeps the bot's own icon." }
55
+ }
56
+ },
41
57
  "ColumnName": {
42
58
  "type": "string",
43
59
  "pattern": "^[A-Za-z_][A-Za-z0-9_]{0,62}$",
@@ -71,12 +87,13 @@
71
87
  "resources": { "$ref": "#/definitions/ResourceNames" }
72
88
  }
73
89
  },
74
- "AppModeChannelOperation": {
90
+ "DeclaredModeChannelOperation": {
75
91
  "type": "object",
76
92
  "additionalProperties": false,
77
93
  "required": ["identity", "resources"],
94
+ "description": "slack.message.post: the app declares the mode — app (posts as the company's Slack app) or user (posts as the signed-in person).",
78
95
  "properties": {
79
- "identity": { "type": "string", "const": "app" },
96
+ "identity": { "$ref": "#/definitions/IntegrationIdentityMode" },
80
97
  "resources": { "$ref": "#/definitions/ResourceNames" }
81
98
  }
82
99
  },
@@ -101,13 +118,14 @@
101
118
  "required": ["kind", "operations"],
102
119
  "properties": {
103
120
  "kind": { "$ref": "#/definitions/IntegrationConnectionKind" },
121
+ "presentation": { "$ref": "#/definitions/IntegrationPresentation", "description": "saas connections only (Slack): how this app's app-mode posts are presented; meaningless for every other operation." },
104
122
  "operations": {
105
123
  "type": "object",
106
124
  "additionalProperties": false,
107
125
  "minProperties": 1,
108
126
  "properties": {
109
127
  "slack.channel.history": { "$ref": "#/definitions/UserModeChannelOperation" },
110
- "slack.message.post": { "$ref": "#/definitions/AppModeChannelOperation" },
128
+ "slack.message.post": { "$ref": "#/definitions/DeclaredModeChannelOperation" },
111
129
  "gmail.thread.list": { "$ref": "#/definitions/UserModeChannelOperation" },
112
130
  "gmail.message.read": { "$ref": "#/definitions/UserModeChannelOperation" },
113
131
  "gmail.message.send": { "$ref": "#/definitions/UserModeChannelOperation" },
@@ -118,6 +136,7 @@
118
136
  "if": { "properties": { "kind": { "const": "saas" } } },
119
137
  "then": { "properties": { "operations": { "not": { "required": ["warehouse.view.read"] } } } },
120
138
  "else": {
139
+ "not": { "required": ["presentation"] },
121
140
  "properties": {
122
141
  "operations": {
123
142
  "not": { "anyOf": [{ "required": ["slack.channel.history"] }, { "required": ["slack.message.post"] }, { "required": ["gmail.thread.list"] }, { "required": ["gmail.message.read"] }, { "required": ["gmail.message.send"] }] }
@@ -132,6 +151,7 @@
132
151
  "connections": {
133
152
  "company-slack": {
134
153
  "kind": "saas",
154
+ "presentation": { "displayName": "Lunch Vote", "iconEmoji": ":sandwich:" },
135
155
  "operations": {
136
156
  "slack.channel.history": { "identity": "user", "resources": ["team-updates"] },
137
157
  "slack.message.post": { "identity": "app", "resources": ["team-updates"] }
@@ -155,6 +175,17 @@
155
175
  }
156
176
  }
157
177
  }
178
+ },
179
+ {
180
+ "schema": "harbour.app-integrations/2.0",
181
+ "connections": {
182
+ "company-slack": {
183
+ "kind": "saas",
184
+ "operations": {
185
+ "slack.message.post": { "identity": "user", "resources": ["team-updates"] }
186
+ }
187
+ }
188
+ }
158
189
  }
159
190
  ]
160
191
  }
@@ -2,7 +2,7 @@
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
3
  "$id": "https://schemas.harbour.dev/data-grant-v2.schema.json",
4
4
  "title": "DataGrantV2",
5
- "description": "An app's integration grant for one connection, environment and identity mode (control table sk APP#<appId>#IGRANT#<environment>#<sourceId>#<mode>; grantId = base64url(JSON [\"v2\",appId,environment,sourceId,mode])). Additive to the legacy DataGrant (data-grant.schema.json), which is never consulted by v2 paths. A fresh REQUESTED record carries empty `operations`/`resources` until the attached request is approved; approved resources are structured `{name, columns?}` entries (a Slack channel alias, the fixed Gmail `inbox` alias, or a warehouse view alias with its approved columns). Expiry is enforced from UTC time on every call and SDK calls never extend it; `expiredAt` is stamped by the recovery sweep afterwards. Status edges live in x-transitions; DENIED and REVOKED are terminal (a regrant after revocation is a new REQUESTED record once cleanup is DONE). Database grants add credentialRef/mintedPrincipal; both hold references only. `identityMode` and `installation` are the deprecated 0.45.0 spellings: the record carries `mode`, and a SaaS grant runs on the connection's single installation.",
5
+ "description": "An app's integration grant for one connection, environment and identity mode (control table sk APP#<appId>#IGRANT#<environment>#<sourceId>#<mode>; grantId = base64url(JSON [\"v2\",appId,environment,sourceId,mode])). Additive to the legacy DataGrant (data-grant.schema.json), which is never consulted by v2 paths. A fresh REQUESTED record carries empty `operations`/`resources` until the attached request is approved; approved resources are structured `{name, columns?}` entries (a Slack channel alias, the fixed Gmail `inbox` alias, or a warehouse view alias with its approved columns). Expiry is enforced from UTC time on every call and SDK calls never extend it; `expiredAt` is stamped by the recovery sweep afterwards. Status edges live in x-transitions; DENIED and REVOKED are terminal (a regrant after revocation is a new REQUESTED record once cleanup is DONE). Database grants add credentialRef/mintedPrincipal; both hold references only. An app-mode Slack grant may carry `presentation` (0.53.0): the IT-approved display name and icon the app's posts go out under, copied from the approved request and read by every post — never from the call. `identityMode` and `installation` are the deprecated 0.45.0 spellings: the record carries `mode`, and a SaaS grant runs on the connection's single installation.",
6
6
  "type": "object",
7
7
  "additionalProperties": false,
8
8
  "required": ["schema", "tenantId", "appId", "environment", "sourceId", "mode", "kind", "status", "operations", "resources", "version", "provisioning", "createdAt", "updatedAt"],
@@ -35,6 +35,7 @@
35
35
  "approvedAt": { "type": "string", "format": "date-time" },
36
36
  "version": { "type": "integer", "minimum": 1, "description": "Optimistic-concurrency version; approvals compare it with the reviewed value." },
37
37
  "pendingRequestId": { "type": "string", "minLength": 1, "maxLength": 128, "description": "The one open access request attached to this grant, if any." },
38
+ "presentation": { "$ref": "app-integrations-declaration.schema.json#/definitions/IntegrationPresentation", "description": "App-mode Slack grants only: how the app's posts are presented, as IT approved it. Absent means the posts carry the bot's own name." },
38
39
  "provisioning": { "$ref": "#/definitions/DataGrantV2Provisioning" },
39
40
  "credentialRef": {
40
41
  "type": "string",
@@ -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; 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 NOT a caller field: it is fixed by the operation (declared per app for slack.message.post) and enforced from the 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 Harbour\" footer) or as the signed-in person (no name, no footer), whichever mode the app declared and IT approved — the same input either way. 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.",
6
6
  "type": "object",
7
7
  "additionalProperties": false,
8
8
  "required": ["connection", "operation", "resource", "input"],
@@ -94,7 +94,8 @@
94
94
  "workspaceId": { "type": "string", "minLength": 1, "maxLength": 128 },
95
95
  "botUserId": { "type": "string", "maxLength": 128, "description": "Empty only for a legacy record normalised without one." },
96
96
  "status": { "$ref": "#/definitions/SaasInstallationStatus" },
97
- "installedAt": { "type": "string", "format": "date-time" }
97
+ "installedAt": { "type": "string", "format": "date-time" },
98
+ "scopes": { "type": "array", "maxItems": 64, "uniqueItems": true, "items": { "type": "string", "minLength": 1, "maxLength": 64 }, "description": "The bot scopes Slack granted at installation (0.53.0). Absent on installations recorded before it, which therefore lack chat:write.customize: per-app display names need a reinstall." }
98
99
  }
99
100
  },
100
101
  "DevelopmentPreapproval": {
@@ -144,7 +145,7 @@
144
145
  "displayName": "Company Slack",
145
146
  "status": "ACTIVE",
146
147
  "version": 3,
147
- "installation": { "tokenRecordId": "tok_workspace", "workspaceId": "T0WORKSPACE", "botUserId": "U0BOT", "status": "installed", "installedAt": "2026-09-09T08:00:00Z" },
148
+ "installation": { "tokenRecordId": "tok_workspace", "workspaceId": "T0WORKSPACE", "botUserId": "U0BOT", "status": "installed", "installedAt": "2026-09-09T08:00:00Z", "scopes": ["channels:read", "chat:write", "chat:write.customize"] },
148
149
  "channels": { "team-updates": "C0123TEAM" },
149
150
  "operations": ["slack.channel.history", "slack.message.post"],
150
151
  "developmentPreapproval": { "enabled": true, "hours": 24 },
@@ -2,7 +2,7 @@
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
3
  "$id": "https://schemas.harbour.dev/transform-report.schema.json",
4
4
  "title": "TransformReport",
5
- "description": "Structured output an agent must return from one Harbour transformation attempt. Handed to the agent as its output schema, then decoded and semantically re-validated by the control plane, which remains the authority. The enum on applied_playbooks.items is the one dynamic part: the adapter injects the names of the playbooks supplied for that invocation before writing the document, so the model is structurally prevented from naming a playbook it was not given. Application-secret handling was removed platform-wide, so this document carries no redactions and no secrets fields. A deployable report must also carry preservation_plan: the agent's own grounded smoke tests (one to three happy_path user journeys plus a boundary_check per behaviour its playbooks or edits could regress) that the validation cell executes after transformation, and an uncovered list naming every boundary it could not test and why. It also carries validation_fixtures: null when no external fixture is needed, or a bounded generic HTTP/WebSocket fixture declaration for that one run. An unsupported report carries only application.unsupported and no plan or fixtures.",
5
+ "description": "Structured output an agent must return from one Harbour transformation attempt. Handed to the agent as its output schema, then decoded and semantically re-validated by the control plane, which remains the authority. The enum on applied_playbooks.items is the one dynamic part: the adapter injects the names of the playbooks supplied for that invocation before writing the document, so the model is structurally prevented from naming a playbook it was not given. Application-secret handling was removed platform-wide, so this document carries no redactions and no secrets fields. A deployable report must also carry preservation_plan: the agent's own grounded smoke tests (one to three happy_path user journeys plus a boundary_check per behaviour its playbooks or edits could regress) that the validation cell executes after transformation, and an uncovered list naming every boundary it could not test and why. It may also carry validation_fixtures: null when no external fixture is needed, or a bounded generic HTTP/WebSocket fixture declaration for that one run; the deployment plane reads the workspace fixture sidecar, so the report's copy is optional. An unsupported report carries only application.unsupported and no plan or fixtures.",
6
6
  "x-go-embed": true,
7
7
  "type": "object",
8
8
  "additionalProperties": false,
@@ -14,26 +14,34 @@
14
14
  "minItems": 1,
15
15
  "items": { "type": "string", "minLength": 1 }
16
16
  },
17
- "static_rebuild_required": { "type": "boolean" },
17
+ "static_rebuild_required": {
18
+ "description": "Optional. The deployed value is a platform constant supplied to the build, not a report field, so a deployable report may omit it and one that still carries it stays valid.",
19
+ "type": "boolean"
20
+ },
18
21
  "analysis": {
19
22
  "type": "object",
20
23
  "additionalProperties": false,
21
- "required": ["deployable", "needsTransformation", "blockers", "surfaces", "evidence"],
24
+ "required": ["deployable", "blockers"],
22
25
  "properties": {
23
26
  "deployable": { "type": "boolean" },
24
- "needsTransformation": { "type": "boolean" },
27
+ "needsTransformation": {
28
+ "description": "Whether the attempt changed the tree. Optional: Harbour derives this from the transformed workspace, so a deployable report may omit it and one that still carries it stays valid.",
29
+ "type": "boolean"
30
+ },
25
31
  "blockers": {
26
32
  "type": "array",
27
33
  "maxItems": 64,
28
34
  "items": { "type": "string", "minLength": 1, "maxLength": 1024 }
29
35
  },
30
36
  "surfaces": {
37
+ "description": "Paths the attempt touched. Optional: Harbour derives this from the transformed workspace, so a deployable report may omit it and one that still carries it stays valid.",
31
38
  "type": "array",
32
39
  "minItems": 1,
33
40
  "maxItems": 1024,
34
41
  "items": { "type": "string", "minLength": 1, "maxLength": 512 }
35
42
  },
36
43
  "evidence": {
44
+ "description": "Optional file-and-line citations for why the transformation was needed. Nothing downstream reads them, so a deployable report may omit them and one that still carries them stays valid.",
37
45
  "type": "array",
38
46
  "maxItems": 1024,
39
47
  "items": {
@@ -52,35 +60,36 @@
52
60
  "database": {
53
61
  "type": "object",
54
62
  "additionalProperties": false,
55
- "required": [
56
- "mode",
57
- "changeType",
58
- "requiresCodeChanges",
59
- "requiresSchemaMigration",
60
- "migrationPaths",
61
- "changedFiles",
62
- "evidence",
63
- "unsupportedFeatures"
64
- ],
63
+ "required": ["mode"],
65
64
  "properties": {
66
65
  "mode": { "type": "string", "enum": ["none", "sql"] },
67
66
  "changeType": {
67
+ "description": "Classification of the changed paths. Optional: Harbour derives this from the transformed workspace, so a deployable report may omit it and one that still carries it stays valid.",
68
68
  "type": "string",
69
69
  "enum": ["none", "code_only", "schema_only", "code_and_schema"]
70
70
  },
71
- "requiresCodeChanges": { "type": "boolean" },
72
- "requiresSchemaMigration": { "type": "boolean" },
71
+ "requiresCodeChanges": {
72
+ "description": "Whether any changed path sits outside the database boundary. Optional: Harbour derives this from the transformed workspace, so a deployable report may omit it and one that still carries it stays valid.",
73
+ "type": "boolean"
74
+ },
75
+ "requiresSchemaMigration": {
76
+ "description": "Whether the Aurora bundle grew past its seeded prefix. Optional: Harbour derives this from the transformed workspace, so a deployable report may omit it and one that still carries it stays valid.",
77
+ "type": "boolean"
78
+ },
73
79
  "migrationPaths": {
80
+ "description": "Migration files in the Aurora bundle. Optional: Harbour derives this from the transformed workspace, so a deployable report may omit it and one that still carries it stays valid.",
74
81
  "type": "array",
75
82
  "maxItems": 128,
76
83
  "items": { "type": "string", "minLength": 1, "maxLength": 512 }
77
84
  },
78
85
  "changedFiles": {
86
+ "description": "Changed paths on the database boundary. Optional: Harbour derives this from the transformed workspace, so a deployable report may omit it and one that still carries it stays valid.",
79
87
  "type": "array",
80
88
  "maxItems": 512,
81
89
  "items": { "type": "string", "minLength": 1, "maxLength": 512 }
82
90
  },
83
91
  "evidence": {
92
+ "description": "Optional file-and-line citations for the database findings. Nothing downstream reads them, so a deployable report may omit them and one that still carries them stays valid.",
84
93
  "type": "array",
85
94
  "maxItems": 128,
86
95
  "items": {
@@ -95,6 +104,7 @@
95
104
  }
96
105
  },
97
106
  "unsupportedFeatures": {
107
+ "description": "Optional. The platform's only structural decline is application.unsupported; nothing reads this list, so a deployable report may omit it and one that still carries it stays valid.",
98
108
  "type": "array",
99
109
  "maxItems": 64,
100
110
  "items": { "type": "string", "minLength": 1, "maxLength": 512 }
@@ -116,13 +126,13 @@
116
126
  "items": {
117
127
  "type": "object",
118
128
  "additionalProperties": false,
119
- "required": ["id", "kind", "name", "boundaries", "evidence", "confidence", "steps"],
129
+ "required": ["id", "kind", "name", "evidence", "confidence", "steps"],
120
130
  "properties": {
121
131
  "id": { "type": "string", "pattern": "^[a-z][a-z0-9-]{0,63}$" },
122
132
  "kind": { "type": "string", "enum": ["happy_path", "boundary_check"] },
123
133
  "name": { "type": "string", "minLength": 1, "maxLength": 120 },
124
134
  "boundaries": {
125
- "description": "Transformation boundaries this test claims to cover.",
135
+ "description": "Transformation boundaries this test claims to cover. Optional: Harbour derives them from the test's own evidence paths, so a deployable report may omit them and one that still carries them stays valid.",
126
136
  "type": "array",
127
137
  "minItems": 1,
128
138
  "maxItems": 8,
@@ -202,9 +212,10 @@
202
212
  "items": {
203
213
  "type": "object",
204
214
  "additionalProperties": false,
205
- "required": ["boundary", "reason"],
215
+ "required": ["reason"],
206
216
  "properties": {
207
217
  "boundary": {
218
+ "description": "Optional: Harbour derives the uncovered boundaries from the changed paths and the covered set, so a deployable report may omit this and one that still carries it stays valid.",
208
219
  "type": "string",
209
220
  "enum": ["application", "routing", "database", "authentication", "runtime_dependency", "files", "provider", "background_work"]
210
221
  },
@@ -390,8 +401,13 @@
390
401
  { "type": "null" }
391
402
  ]
392
403
  },
393
- "buildKind": { "type": "string", "enum": ["container", "static"] },
404
+ "buildKind": {
405
+ "description": "Optional: determined by topology (static topology iff static build), so a deployable report may omit it and one that still carries it stays valid.",
406
+ "type": "string",
407
+ "enum": ["container", "static"]
408
+ },
394
409
  "topology": {
410
+ "description": "Optional: determined by the build output kinds, so a deployable report may omit it and one that still carries it stays valid.",
395
411
  "type": "string",
396
412
  "enum": ["static", "backend", "frontend_and_backend"]
397
413
  },
@@ -434,6 +450,7 @@
434
450
  }
435
451
  },
436
452
  "basePath": {
453
+ "description": "Optional: both members are platform constants, so a deployable report may omit this object and one that still carries it stays valid.",
437
454
  "type": "object",
438
455
  "additionalProperties": false,
439
456
  "required": ["required", "environmentVariable"],
@@ -458,6 +475,7 @@
458
475
  }
459
476
  },
460
477
  "unsupportedFeatures": {
478
+ "description": "Optional: a deployable plan requires this empty, so a deployable report may omit it and one that still carries it (empty) stays valid.",
461
479
  "type": "array",
462
480
  "maxItems": 64,
463
481
  "items": { "type": "string", "minLength": 1, "maxLength": 512 }
@@ -667,10 +685,10 @@
667
685
  ],
668
686
  "oneOf": [
669
687
  {
670
- "required": ["static_rebuild_required", "analysis", "database", "preservation_plan", "validation_fixtures", "application"],
688
+ "required": ["analysis", "database", "preservation_plan", "application"],
671
689
  "properties": {
672
690
  "application": {
673
- "required": ["runtime", "buildKind", "topology", "entrypoint", "build", "basePath", "evidence", "unsupportedFeatures"],
691
+ "required": ["runtime", "entrypoint", "build", "evidence"],
674
692
  "not": { "required": ["unsupported"] }
675
693
  }
676
694
  }