@vellumai/assistant 0.11.10-dev.202609091820.7951ec9 → 0.11.10-dev.202609092016.c7aa45c
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -1
- package/node_modules/@vellumai/gateway-client/src/guardian-request-contract.ts +9 -4
- package/node_modules/@vellumai/gateway-client/src/ipc-client.ts +5 -3
- package/openapi.yaml +216 -0
- package/package.json +1 -1
- package/src/__tests__/config-schema.test.ts +26 -0
- package/src/__tests__/db-readiness-http-gate.test.ts +43 -0
- package/src/__tests__/error-handler-friendly-messages.test.ts +15 -0
- package/src/__tests__/guardian-gateway-sim.ts +5 -0
- package/src/__tests__/helpers/gateway-guardian-requests-store-bridge.ts +3 -2
- package/src/__tests__/identity-routes.test.ts +6 -1
- package/src/__tests__/tool-grant-request-escalation.test.ts +43 -0
- package/src/__tests__/workspace-git-service.test.ts +170 -1
- package/src/__tests__/workspace-heartbeat-service.test.ts +41 -0
- package/src/config/bundled-skills/conversation-groups/SKILL.md +1 -0
- package/src/config/bundled-skills/schedule/SKILL.md +10 -0
- package/src/config/schemas/workspace-git.ts +10 -0
- package/src/conversations/message-consolidation.ts +4 -0
- package/src/daemon/__tests__/daemon-readiness.test.ts +32 -0
- package/src/daemon/daemon-readiness.ts +34 -4
- package/src/daemon/lifecycle.ts +12 -5
- package/src/daemon/shutdown-handlers.ts +2 -0
- package/src/ipc/__tests__/db-readiness-ipc-gate.test.ts +17 -2
- package/src/ipc/__tests__/gateway-ipc-errors.test.ts +51 -0
- package/src/ipc/__tests__/gateway-validated-call.test.ts +5 -6
- package/src/ipc/assistant-server.ts +9 -16
- package/src/ipc/gateway-client.test.ts +6 -2
- package/src/ipc/gateway-client.ts +53 -14
- package/src/ipc/gateway-ipc-errors.ts +16 -4
- package/src/ipc/gateway-validated-call.ts +5 -12
- package/src/monitoring/__tests__/workspace-git-heartbeat.test.ts +57 -0
- package/src/monitoring/worker.ts +18 -0
- package/src/monitoring/workspace-git-heartbeat.ts +45 -0
- package/src/notifications/__tests__/notification-utils.test.ts +13 -0
- package/src/notifications/__tests__/schedule-result-producer.test.ts +15 -0
- package/src/notifications/notification-utils.ts +15 -0
- package/src/notifications/schedule-result-producer.ts +2 -1
- package/src/persistence/__tests__/db-init-migrations-ok.test.ts +14 -3
- package/src/persistence/conversation-crud.ts +201 -0
- package/src/persistence/db-init.ts +23 -7
- package/src/persistence/migrations/__tests__/run-migrations.test.ts +83 -8
- package/src/persistence/migrations/run-migrations.ts +101 -6
- package/src/runtime/access-request-helper.ts +5 -1
- package/src/runtime/middleware/error-handler.ts +14 -10
- package/src/runtime/routes/attachment-list-route.test.ts +961 -0
- package/src/runtime/routes/attachment-routes.ts +128 -0
- package/src/runtime/routes/debug-routes.ts +64 -0
- package/src/runtime/routes/gateway-log-routes.ts +1 -8
- package/src/runtime/routes/http-adapter.ts +8 -15
- package/src/runtime/routes/identity-routes.ts +13 -0
- package/src/runtime/routes/trust-rules-routes.ts +1 -8
- package/src/runtime/tool-grant-request-helper.ts +6 -2
- package/src/workspace/git-service.ts +217 -57
- package/src/workspace/heartbeat-service.ts +34 -9
package/AGENTS.md
CHANGED
|
@@ -30,7 +30,7 @@ DB migrations run asynchronously during startup: the HTTP server binds (so `/hea
|
|
|
30
30
|
|
|
31
31
|
Existing enforcement, which new code must not bypass:
|
|
32
32
|
|
|
33
|
-
- **HTTP** requests are gated per-route in `runtime/http-server.ts`; **IPC** methods in `ipc/assistant-server.ts`; both derive their exempt set from `DB_MIGRATION_READINESS_EXEMPT_OPERATIONS` in `daemon-readiness.ts` (health/liveness probes
|
|
33
|
+
- **HTTP** requests are gated per-route in `runtime/http-server.ts`; **IPC** methods in `ipc/assistant-server.ts`; both derive their exempt set from `DB_MIGRATION_READINESS_EXEMPT_OPERATIONS` in `daemon-readiness.ts` (health/liveness probes plus in-memory diagnostics such as `debug/database` — anything exempted must never touch the DB).
|
|
34
34
|
- **Message sinks** (`processMessage`, `processMessageInBackground`) guard via `assertDbMigrationsReadyForTurn()`.
|
|
35
35
|
- **Background sweeps** are started by lifecycle only after migrations settle (`startRuntimeHttpServerBackgroundSweeps`).
|
|
36
36
|
- The **migration-repair surface** (`admin/rollback-migrations` plus all `migrations/import*` / preflight transports and their job-status route) is additionally allowed in the terminal `failed` state only — see `DB_MIGRATION_FAILED_STATE_EXEMPT_OPERATIONS`. Never widen this to the `running` state: a rollback or import would race the in-flight migration runner. A successful repair does not clear the failed latch — the daemon must be restarted to re-run migrations and become ready.
|
|
@@ -254,10 +254,15 @@ export type GuardianRequestMutationIpcResponse = z.infer<
|
|
|
254
254
|
|
|
255
255
|
/**
|
|
256
256
|
* Request for `guardian_requests_create`. `id` is REQUIRED and
|
|
257
|
-
* caller-supplied
|
|
258
|
-
*
|
|
259
|
-
* requestId as PK
|
|
260
|
-
*
|
|
257
|
+
* caller-supplied. The interaction-promotion paths (`tool_approval`, and
|
|
258
|
+
* `pending_question` for `ask_question`) reuse the pending-interaction
|
|
259
|
+
* requestId as PK so a decision can find its interaction; every other
|
|
260
|
+
* create mints a UUID, including voice `pending_question` rows, which carry
|
|
261
|
+
* their interaction id in `pendingQuestionId` instead. The id is the row's
|
|
262
|
+
* primary key and the insert is strict, so a caller that can create
|
|
263
|
+
* concurrently must mint one unique per call. `requestCode` is generated
|
|
264
|
+
* gateway-side when omitted. No `sourceType`: the gateway derives it from
|
|
265
|
+
* `sourceChannel`.
|
|
261
266
|
*/
|
|
262
267
|
export const CreateGuardianRequestIpcParamsSchema = z.object({
|
|
263
268
|
id: z.string().min(1),
|
|
@@ -340,7 +340,8 @@ type PendingRequest = {
|
|
|
340
340
|
export type PersistentIpcClientOptions = {
|
|
341
341
|
/**
|
|
342
342
|
* Additional delays (ms) before each reconnect attempt after a retryable
|
|
343
|
-
* connect failure. Pass `[]` to fail on the first miss (tests
|
|
343
|
+
* connect failure. Pass `[]` to fail on the first miss (tests and callers
|
|
344
|
+
* that already own a retry budget, such as classify_risk).
|
|
344
345
|
*/
|
|
345
346
|
connectRetryBackoffsMs?: readonly number[];
|
|
346
347
|
};
|
|
@@ -350,8 +351,9 @@ export type PersistentIpcClientOptions = {
|
|
|
350
351
|
* reconnection on failure. Multiplexes requests by ID so many concurrent
|
|
351
352
|
* callers can share one socket.
|
|
352
353
|
*
|
|
353
|
-
* Designed for
|
|
354
|
-
*
|
|
354
|
+
* Designed for multiplexed calls over one socket. Control-plane callers
|
|
355
|
+
* typically keep the default connect retries for sibling boot races.
|
|
356
|
+
* Callers that already own a retry budget pass `connectRetryBackoffsMs: []`.
|
|
355
357
|
*/
|
|
356
358
|
export class PersistentIpcClient {
|
|
357
359
|
private socket: Socket | null = null;
|
package/openapi.yaml
CHANGED
|
@@ -1713,6 +1713,99 @@ paths:
|
|
|
1713
1713
|
responses:
|
|
1714
1714
|
"204":
|
|
1715
1715
|
description: Successful response
|
|
1716
|
+
get:
|
|
1717
|
+
operationId: attachments_get
|
|
1718
|
+
summary: List conversation attachments
|
|
1719
|
+
description:
|
|
1720
|
+
Return metadata for every attachment linked to a conversation's messages, newest first, across fork
|
|
1721
|
+
lineage. Camera frames kept by live vision are flagged. Bytes come from GET /attachments/{id}/content.
|
|
1722
|
+
tags:
|
|
1723
|
+
- attachments
|
|
1724
|
+
parameters:
|
|
1725
|
+
- name: conversationId
|
|
1726
|
+
in: query
|
|
1727
|
+
required: true
|
|
1728
|
+
schema:
|
|
1729
|
+
type: string
|
|
1730
|
+
description: Conversation whose attachments to list
|
|
1731
|
+
- name: sightFrames
|
|
1732
|
+
in: query
|
|
1733
|
+
required: false
|
|
1734
|
+
schema:
|
|
1735
|
+
type: string
|
|
1736
|
+
enum:
|
|
1737
|
+
- only
|
|
1738
|
+
- exclude
|
|
1739
|
+
description: Restrict to camera frames, or leave them out
|
|
1740
|
+
- name: limit
|
|
1741
|
+
in: query
|
|
1742
|
+
required: false
|
|
1743
|
+
schema:
|
|
1744
|
+
type: integer
|
|
1745
|
+
description: Page size, default 200, max 1000
|
|
1746
|
+
- name: offset
|
|
1747
|
+
in: query
|
|
1748
|
+
required: false
|
|
1749
|
+
schema:
|
|
1750
|
+
type: integer
|
|
1751
|
+
responses:
|
|
1752
|
+
"200":
|
|
1753
|
+
description: Successful response
|
|
1754
|
+
content:
|
|
1755
|
+
application/json:
|
|
1756
|
+
schema:
|
|
1757
|
+
type: object
|
|
1758
|
+
properties:
|
|
1759
|
+
attachments:
|
|
1760
|
+
type: array
|
|
1761
|
+
items:
|
|
1762
|
+
type: object
|
|
1763
|
+
properties:
|
|
1764
|
+
id:
|
|
1765
|
+
type: string
|
|
1766
|
+
filename:
|
|
1767
|
+
type: string
|
|
1768
|
+
mimeType:
|
|
1769
|
+
type: string
|
|
1770
|
+
sizeBytes:
|
|
1771
|
+
type: number
|
|
1772
|
+
kind:
|
|
1773
|
+
type: string
|
|
1774
|
+
messageId:
|
|
1775
|
+
type: string
|
|
1776
|
+
createdAt:
|
|
1777
|
+
type: number
|
|
1778
|
+
description: "Capture time: the carrying message's created_at"
|
|
1779
|
+
fileBacked:
|
|
1780
|
+
type: boolean
|
|
1781
|
+
thumbnailData:
|
|
1782
|
+
type: string
|
|
1783
|
+
sightFrame:
|
|
1784
|
+
type: boolean
|
|
1785
|
+
ambientKeep:
|
|
1786
|
+
type: boolean
|
|
1787
|
+
required:
|
|
1788
|
+
- id
|
|
1789
|
+
- filename
|
|
1790
|
+
- mimeType
|
|
1791
|
+
- sizeBytes
|
|
1792
|
+
- kind
|
|
1793
|
+
- messageId
|
|
1794
|
+
- createdAt
|
|
1795
|
+
- sightFrame
|
|
1796
|
+
- ambientKeep
|
|
1797
|
+
additionalProperties: false
|
|
1798
|
+
total:
|
|
1799
|
+
type: number
|
|
1800
|
+
hasMore:
|
|
1801
|
+
type: boolean
|
|
1802
|
+
required:
|
|
1803
|
+
- attachments
|
|
1804
|
+
- total
|
|
1805
|
+
- hasMore
|
|
1806
|
+
additionalProperties: false
|
|
1807
|
+
"400":
|
|
1808
|
+
description: Missing or invalid query
|
|
1716
1809
|
post:
|
|
1717
1810
|
operationId: attachments_post
|
|
1718
1811
|
summary: Upload attachment
|
|
@@ -11081,6 +11174,71 @@ paths:
|
|
|
11081
11174
|
- exitCode
|
|
11082
11175
|
- timedOut
|
|
11083
11176
|
additionalProperties: false
|
|
11177
|
+
/v1/debug/database:
|
|
11178
|
+
get:
|
|
11179
|
+
operationId: debug_database_get
|
|
11180
|
+
summary: Database migration diagnostics
|
|
11181
|
+
description:
|
|
11182
|
+
Return the in-memory DB migration latch, including failed and deferred steps. Does not query SQLite, so it
|
|
11183
|
+
stays answerable while migrations are running or have failed.
|
|
11184
|
+
tags:
|
|
11185
|
+
- debug
|
|
11186
|
+
responses:
|
|
11187
|
+
"200":
|
|
11188
|
+
description: Successful response
|
|
11189
|
+
content:
|
|
11190
|
+
application/json:
|
|
11191
|
+
schema:
|
|
11192
|
+
type: object
|
|
11193
|
+
properties:
|
|
11194
|
+
ready:
|
|
11195
|
+
type: boolean
|
|
11196
|
+
state:
|
|
11197
|
+
type: string
|
|
11198
|
+
enum:
|
|
11199
|
+
- not_started
|
|
11200
|
+
- running
|
|
11201
|
+
- failed
|
|
11202
|
+
- ready
|
|
11203
|
+
reason:
|
|
11204
|
+
type: string
|
|
11205
|
+
error:
|
|
11206
|
+
type: string
|
|
11207
|
+
failed:
|
|
11208
|
+
type: array
|
|
11209
|
+
items:
|
|
11210
|
+
type: object
|
|
11211
|
+
properties:
|
|
11212
|
+
name:
|
|
11213
|
+
type: string
|
|
11214
|
+
error:
|
|
11215
|
+
type: string
|
|
11216
|
+
required:
|
|
11217
|
+
- name
|
|
11218
|
+
additionalProperties: false
|
|
11219
|
+
deferred:
|
|
11220
|
+
type: array
|
|
11221
|
+
items:
|
|
11222
|
+
type: object
|
|
11223
|
+
properties:
|
|
11224
|
+
name:
|
|
11225
|
+
type: string
|
|
11226
|
+
missing:
|
|
11227
|
+
type: array
|
|
11228
|
+
items:
|
|
11229
|
+
type: string
|
|
11230
|
+
required:
|
|
11231
|
+
- name
|
|
11232
|
+
- missing
|
|
11233
|
+
additionalProperties: false
|
|
11234
|
+
validationError:
|
|
11235
|
+
type: string
|
|
11236
|
+
required:
|
|
11237
|
+
- ready
|
|
11238
|
+
- state
|
|
11239
|
+
- failed
|
|
11240
|
+
- deferred
|
|
11241
|
+
additionalProperties: false
|
|
11084
11242
|
/v1/defer/cancel:
|
|
11085
11243
|
post:
|
|
11086
11244
|
operationId: defer_cancel_post
|
|
@@ -13442,6 +13600,35 @@ paths:
|
|
|
13442
13600
|
type: string
|
|
13443
13601
|
error:
|
|
13444
13602
|
type: string
|
|
13603
|
+
failedMigrations:
|
|
13604
|
+
type: array
|
|
13605
|
+
items:
|
|
13606
|
+
type: object
|
|
13607
|
+
properties:
|
|
13608
|
+
name:
|
|
13609
|
+
type: string
|
|
13610
|
+
error:
|
|
13611
|
+
type: string
|
|
13612
|
+
required:
|
|
13613
|
+
- name
|
|
13614
|
+
additionalProperties: false
|
|
13615
|
+
deferredMigrations:
|
|
13616
|
+
type: array
|
|
13617
|
+
items:
|
|
13618
|
+
type: object
|
|
13619
|
+
properties:
|
|
13620
|
+
name:
|
|
13621
|
+
type: string
|
|
13622
|
+
missing:
|
|
13623
|
+
type: array
|
|
13624
|
+
items:
|
|
13625
|
+
type: string
|
|
13626
|
+
required:
|
|
13627
|
+
- name
|
|
13628
|
+
- missing
|
|
13629
|
+
additionalProperties: false
|
|
13630
|
+
validationError:
|
|
13631
|
+
type: string
|
|
13445
13632
|
required:
|
|
13446
13633
|
- ready
|
|
13447
13634
|
- state
|
|
@@ -13647,6 +13834,35 @@ paths:
|
|
|
13647
13834
|
type: string
|
|
13648
13835
|
error:
|
|
13649
13836
|
type: string
|
|
13837
|
+
failedMigrations:
|
|
13838
|
+
type: array
|
|
13839
|
+
items:
|
|
13840
|
+
type: object
|
|
13841
|
+
properties:
|
|
13842
|
+
name:
|
|
13843
|
+
type: string
|
|
13844
|
+
error:
|
|
13845
|
+
type: string
|
|
13846
|
+
required:
|
|
13847
|
+
- name
|
|
13848
|
+
additionalProperties: false
|
|
13849
|
+
deferredMigrations:
|
|
13850
|
+
type: array
|
|
13851
|
+
items:
|
|
13852
|
+
type: object
|
|
13853
|
+
properties:
|
|
13854
|
+
name:
|
|
13855
|
+
type: string
|
|
13856
|
+
missing:
|
|
13857
|
+
type: array
|
|
13858
|
+
items:
|
|
13859
|
+
type: string
|
|
13860
|
+
required:
|
|
13861
|
+
- name
|
|
13862
|
+
- missing
|
|
13863
|
+
additionalProperties: false
|
|
13864
|
+
validationError:
|
|
13865
|
+
type: string
|
|
13650
13866
|
required:
|
|
13651
13867
|
- ready
|
|
13652
13868
|
- state
|
package/package.json
CHANGED
|
@@ -757,6 +757,7 @@ describe("AssistantConfigSchema", () => {
|
|
|
757
757
|
maxFileSizeBytes: 256000,
|
|
758
758
|
historyCompaction: { enabled: true },
|
|
759
759
|
interactiveGitTimeoutMs: 10000,
|
|
760
|
+
stageBatchSize: 1000,
|
|
760
761
|
enrichmentQueueSize: 50,
|
|
761
762
|
enrichmentConcurrency: 1,
|
|
762
763
|
enrichmentJobTimeoutMs: 30000,
|
|
@@ -811,6 +812,31 @@ describe("AssistantConfigSchema", () => {
|
|
|
811
812
|
expect(result.success).toBe(false);
|
|
812
813
|
});
|
|
813
814
|
|
|
815
|
+
test("applies workspaceGit.stageBatchSize default", () => {
|
|
816
|
+
expect(AssistantConfigSchema.parse({}).workspaceGit.stageBatchSize).toBe(
|
|
817
|
+
1000,
|
|
818
|
+
);
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
test("accepts custom workspaceGit.stageBatchSize", () => {
|
|
822
|
+
const result = AssistantConfigSchema.parse({
|
|
823
|
+
workspaceGit: { stageBatchSize: 250 },
|
|
824
|
+
});
|
|
825
|
+
expect(result.workspaceGit.stageBatchSize).toBe(250);
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
test("rejects non-positive workspaceGit.stageBatchSize", () => {
|
|
829
|
+
const zeroResult = AssistantConfigSchema.safeParse({
|
|
830
|
+
workspaceGit: { stageBatchSize: 0 },
|
|
831
|
+
});
|
|
832
|
+
expect(zeroResult.success).toBe(false);
|
|
833
|
+
|
|
834
|
+
const negativeResult = AssistantConfigSchema.safeParse({
|
|
835
|
+
workspaceGit: { stageBatchSize: -1 },
|
|
836
|
+
});
|
|
837
|
+
expect(negativeResult.success).toBe(false);
|
|
838
|
+
});
|
|
839
|
+
|
|
814
840
|
test("applies sight defaults", () => {
|
|
815
841
|
expect(AssistantConfigSchema.parse({}).sight).toEqual({
|
|
816
842
|
keepLatestFrames: 2,
|
|
@@ -123,6 +123,49 @@ describe("DB migration readiness HTTP gate", () => {
|
|
|
123
123
|
expect(body.reason).toBe("db_migrations_failed");
|
|
124
124
|
});
|
|
125
125
|
|
|
126
|
+
test("continues serving debug/database while migrations are running", async () => {
|
|
127
|
+
setDbMigrating();
|
|
128
|
+
await startServer();
|
|
129
|
+
|
|
130
|
+
const response = await fetch(url("/debug/database"));
|
|
131
|
+
expect(response.status).toBe(200);
|
|
132
|
+
|
|
133
|
+
const body = (await response.json()) as Record<string, unknown>;
|
|
134
|
+
expect(body.ready).toBe(false);
|
|
135
|
+
expect(body.state).toBe("running");
|
|
136
|
+
expect(body.reason).toBe("db_migrations_running");
|
|
137
|
+
expect(body.failed).toEqual([]);
|
|
138
|
+
expect(body.deferred).toEqual([]);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("continues serving debug/database when migrations have failed", async () => {
|
|
142
|
+
setDbMigrationFailed(new Error("boom"), {
|
|
143
|
+
failedMigrations: [{ name: "flakyStep", error: "transient failure" }],
|
|
144
|
+
deferredMigrations: [{ name: "dependentStep", missing: ["flakyStep"] }],
|
|
145
|
+
validationError: "schema mismatch",
|
|
146
|
+
});
|
|
147
|
+
await startServer();
|
|
148
|
+
|
|
149
|
+
const response = await fetch(url("/debug/database"));
|
|
150
|
+
expect(response.status).toBe(200);
|
|
151
|
+
|
|
152
|
+
const body = (await response.json()) as Record<string, unknown>;
|
|
153
|
+
expect(body.ready).toBe(false);
|
|
154
|
+
expect(body.state).toBe("failed");
|
|
155
|
+
expect(body.reason).toBe("db_migrations_failed");
|
|
156
|
+
expect(body.error).toBe("boom");
|
|
157
|
+
expect(body.failed).toEqual([
|
|
158
|
+
{ name: "flakyStep", error: "transient failure" },
|
|
159
|
+
]);
|
|
160
|
+
expect(body.deferred).toEqual([
|
|
161
|
+
{ name: "dependentStep", missing: ["flakyStep"] },
|
|
162
|
+
]);
|
|
163
|
+
expect(body.validationError).toBe("schema mismatch");
|
|
164
|
+
|
|
165
|
+
const conversations = await fetch(url("/conversations"));
|
|
166
|
+
expect(conversations.status).toBe(503);
|
|
167
|
+
});
|
|
168
|
+
|
|
126
169
|
test("blocks config schema while migrations are running", async () => {
|
|
127
170
|
setDbMigrating();
|
|
128
171
|
await startServer();
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
|
|
3
|
+
import { IpcConnectError } from "@vellumai/gateway-client/ipc-client";
|
|
4
|
+
|
|
3
5
|
import { withErrorHandling } from "../runtime/middleware/error-handler.js";
|
|
4
6
|
import { ConfigError, ProviderNotConfiguredError } from "../util/errors.js";
|
|
5
7
|
|
|
@@ -42,4 +44,17 @@ describe("withErrorHandling – friendly error messages", () => {
|
|
|
42
44
|
};
|
|
43
45
|
expect(body.error.message).toBe("Twilio phone number not configured.");
|
|
44
46
|
});
|
|
47
|
+
|
|
48
|
+
test("IpcConnectError surfaces as 503 SERVICE_UNAVAILABLE", async () => {
|
|
49
|
+
const response = await withErrorHandling("test", async () => {
|
|
50
|
+
throw new IpcConnectError("connect ENOENT", "ENOENT");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
expect(response.status).toBe(503);
|
|
54
|
+
const body = (await response.json()) as {
|
|
55
|
+
error: { code: string; message: string };
|
|
56
|
+
};
|
|
57
|
+
expect(body.error.code).toBe("SERVICE_UNAVAILABLE");
|
|
58
|
+
expect(body.error.message).toContain("Gateway is not reachable over IPC");
|
|
59
|
+
});
|
|
45
60
|
});
|
|
@@ -564,6 +564,11 @@ export function createGuardianGatewaySim() {
|
|
|
564
564
|
async function createGuardianRequest(
|
|
565
565
|
params: SeedRequestParams & { id: string; guardianPrincipalId: string },
|
|
566
566
|
): Promise<SimGuardianRequest> {
|
|
567
|
+
// The gateway insert is strict on the primary key; a silent overwrite
|
|
568
|
+
// here would hide a caller minting the same id twice.
|
|
569
|
+
if (requests.has(params.id)) {
|
|
570
|
+
throw new Error("UNIQUE constraint failed: guardian_requests.id");
|
|
571
|
+
}
|
|
567
572
|
return seedRequest(params);
|
|
568
573
|
}
|
|
569
574
|
|
|
@@ -49,8 +49,9 @@ async function createGuardianRequest(
|
|
|
49
49
|
params: CreateGuardianRequestIpcParams,
|
|
50
50
|
): Promise<SimGuardianRequest> {
|
|
51
51
|
// The schema requires `guardianPrincipalId`, mirroring the gateway store's
|
|
52
|
-
// integrity guard (every contract kind is decisionable).
|
|
53
|
-
|
|
52
|
+
// integrity guard (every contract kind is decisionable). The sim's create
|
|
53
|
+
// then rejects a duplicate id, mirroring the store's strict insert.
|
|
54
|
+
return bridgeState.module.createGuardianRequest(
|
|
54
55
|
CreateGuardianRequestIpcParamsSchema.parse(params),
|
|
55
56
|
);
|
|
56
57
|
}
|
|
@@ -483,13 +483,18 @@ describe("identity routes — /readyz readiness gate", () => {
|
|
|
483
483
|
});
|
|
484
484
|
|
|
485
485
|
test("returns 503 when DB migrations fail", async () => {
|
|
486
|
-
setDbMigrationFailed(new Error("migration failed")
|
|
486
|
+
setDbMigrationFailed(new Error("migration failed"), {
|
|
487
|
+
failedMigrations: [{ name: "flakyStep", error: "transient failure" }],
|
|
488
|
+
});
|
|
487
489
|
const res = handleReadyz();
|
|
488
490
|
expect(res.status).toBe(503);
|
|
489
491
|
const body = (await res.json()) as Record<string, unknown>;
|
|
490
492
|
expect(body.ready).toBe(false);
|
|
491
493
|
expect(body.reason).toBe("db_migrations_failed");
|
|
492
494
|
expect((body.dbMigrations as Record<string, unknown>).state).toBe("failed");
|
|
495
|
+
expect((body.dbMigrations as Record<string, unknown>).failedMigrations).toEqual(
|
|
496
|
+
[{ name: "flakyStep", error: "transient failure" }],
|
|
497
|
+
);
|
|
493
498
|
});
|
|
494
499
|
|
|
495
500
|
test("returns 200 even if CES is down", async () => {
|
|
@@ -282,6 +282,49 @@ describe("ToolApprovalHandler / grant-miss escalation", () => {
|
|
|
282
282
|
});
|
|
283
283
|
expect(requests.length).toBe(0);
|
|
284
284
|
});
|
|
285
|
+
|
|
286
|
+
test("sibling tool calls escalating concurrently each get their own request", async () => {
|
|
287
|
+
// The agent loop runs a turn's tool_use blocks under Promise.all, so two
|
|
288
|
+
// grant-gated calls reach the gateway create in the same tick. Each is a
|
|
289
|
+
// distinct invocation (different input digest), so each needs its own
|
|
290
|
+
// row; the create must never collide on the primary key.
|
|
291
|
+
const context = makeContext({ trustClass: "trusted_contact" });
|
|
292
|
+
const results = await Promise.all([
|
|
293
|
+
handler.checkPreExecutionGates(
|
|
294
|
+
"bash",
|
|
295
|
+
{ command: "ls -la" },
|
|
296
|
+
context,
|
|
297
|
+
"high",
|
|
298
|
+
Date.now(),
|
|
299
|
+
),
|
|
300
|
+
handler.checkPreExecutionGates(
|
|
301
|
+
"bash",
|
|
302
|
+
{ command: "cat README.md" },
|
|
303
|
+
context,
|
|
304
|
+
"high",
|
|
305
|
+
Date.now(),
|
|
306
|
+
),
|
|
307
|
+
]);
|
|
308
|
+
|
|
309
|
+
// Both escalate and time out on the short inline wait: a denial for each,
|
|
310
|
+
// never a thrown persistence error that would fail the whole turn.
|
|
311
|
+
for (const result of results) {
|
|
312
|
+
expect(result.allowed).toBe(false);
|
|
313
|
+
if (result.allowed) {
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
expect(result.result.content).toContain("guardian approval");
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const requests = await sim.module.listGuardianRequestsOrEmpty({
|
|
320
|
+
kind: "tool_grant_request",
|
|
321
|
+
status: "pending",
|
|
322
|
+
});
|
|
323
|
+
expect(requests.length).toBe(2);
|
|
324
|
+
expect(new Set(requests.map((r) => r.id)).size).toBe(2);
|
|
325
|
+
expect(new Set(requests.map((r) => r.inputDigest)).size).toBe(2);
|
|
326
|
+
expect(emittedSignals.length).toBe(2);
|
|
327
|
+
});
|
|
285
328
|
});
|
|
286
329
|
|
|
287
330
|
// ---------------------------------------------------------------------------
|