primitive-admin 1.2.0-alpha.3 → 1.2.0-alpha.5
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/assets/skill/skills/primitive-platform/SKILL.md +17 -0
- package/dist/src/commands/agent-sessions.js +4 -0
- package/dist/src/commands/agent-sessions.js.map +1 -1
- package/dist/src/commands/auth.js +7 -4
- package/dist/src/commands/auth.js.map +1 -1
- package/dist/src/commands/connections.js +11 -12
- package/dist/src/commands/connections.js.map +1 -1
- package/dist/src/commands/database-type-configs.js +2 -76
- package/dist/src/commands/database-type-configs.js.map +1 -1
- package/dist/src/commands/database-types.js +313 -13
- package/dist/src/commands/database-types.js.map +1 -1
- package/dist/src/commands/databases.d.ts +9 -0
- package/dist/src/commands/databases.js +78 -403
- package/dist/src/commands/databases.js.map +1 -1
- package/dist/src/commands/documents.d.ts +32 -2
- package/dist/src/commands/documents.js +344 -711
- package/dist/src/commands/documents.js.map +1 -1
- package/dist/src/commands/functions.js +2 -2
- package/dist/src/commands/functions.js.map +1 -1
- package/dist/src/commands/llm.js +6 -9
- package/dist/src/commands/llm.js.map +1 -1
- package/dist/src/commands/prompts.d.ts +7 -0
- package/dist/src/commands/prompts.js +44 -0
- package/dist/src/commands/prompts.js.map +1 -1
- package/dist/src/commands/settings.d.ts +9 -12
- package/dist/src/commands/settings.js +79 -31
- package/dist/src/commands/settings.js.map +1 -1
- package/dist/src/commands/sync.d.ts +43 -110
- package/dist/src/commands/sync.js +356 -1251
- package/dist/src/commands/sync.js.map +1 -1
- package/dist/src/lib/aggregate-output.d.ts +25 -0
- package/dist/src/lib/aggregate-output.js +53 -0
- package/dist/src/lib/aggregate-output.js.map +1 -0
- package/dist/src/lib/api-client.d.ts +48 -204
- package/dist/src/lib/api-client.js +57 -321
- package/dist/src/lib/api-client.js.map +1 -1
- package/dist/src/lib/codegen-shared/generatedFiles.d.ts +6 -7
- package/dist/src/lib/codegen-shared/generatedFiles.js +2 -2
- package/dist/src/lib/codegen-shared/generatedFiles.js.map +1 -1
- package/dist/src/lib/codegen-shared/prettierStable.d.ts +1 -1
- package/dist/src/lib/codegen-shared/prettierStable.js +1 -1
- package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.d.ts +2 -2
- package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.js +2 -2
- package/dist/src/lib/config-object-descriptor.js +15 -10
- package/dist/src/lib/config-object-descriptor.js.map +1 -1
- package/dist/src/lib/config-surface.d.ts +2 -1
- package/dist/src/lib/config-surface.js +4 -1
- package/dist/src/lib/config-surface.js.map +1 -1
- package/dist/src/lib/connections-list.d.ts +10 -0
- package/dist/src/lib/connections-list.js +18 -0
- package/dist/src/lib/connections-list.js.map +1 -0
- package/dist/src/lib/csv.d.ts +8 -9
- package/dist/src/lib/csv.js +3 -4
- package/dist/src/lib/csv.js.map +1 -1
- package/dist/src/lib/db-codegen/dbGenerator.js +5 -16
- package/dist/src/lib/db-codegen/dbGenerator.js.map +1 -1
- package/dist/src/lib/db-codegen/dbNaming.d.ts +5 -13
- package/dist/src/lib/db-codegen/dbNaming.js +5 -17
- package/dist/src/lib/db-codegen/dbNaming.js.map +1 -1
- package/dist/src/lib/db-codegen/dbTemplates.d.ts +7 -56
- package/dist/src/lib/db-codegen/dbTemplates.js +25 -125
- package/dist/src/lib/db-codegen/dbTemplates.js.map +1 -1
- package/dist/src/lib/db-codegen/dbTsTypes.d.ts +2 -2
- package/dist/src/lib/db-codegen/dbTsTypes.js +2 -2
- package/dist/src/lib/db-codegen/dbTypeIR.d.ts +60 -2
- package/dist/src/lib/db-codegen/dbTypeIR.js +12 -1
- package/dist/src/lib/db-codegen/dbTypeIR.js.map +1 -1
- package/dist/src/lib/document-export-record-log.d.ts +57 -0
- package/dist/src/lib/document-export-record-log.js +215 -0
- package/dist/src/lib/document-export-record-log.js.map +1 -0
- package/dist/src/lib/document-ingest-artifact.d.ts +13 -2
- package/dist/src/lib/document-ingest-artifact.js +7 -3
- package/dist/src/lib/document-ingest-artifact.js.map +1 -1
- package/dist/src/lib/document-ingest-rows.d.ts +16 -0
- package/dist/src/lib/document-ingest-rows.js +17 -0
- package/dist/src/lib/document-ingest-rows.js.map +1 -1
- package/dist/src/lib/document-ingest.d.ts +22 -0
- package/dist/src/lib/document-ingest.js +33 -0
- package/dist/src/lib/document-ingest.js.map +1 -1
- package/dist/src/lib/function-sync.d.ts +3 -16
- package/dist/src/lib/function-sync.js +14 -18
- package/dist/src/lib/function-sync.js.map +1 -1
- package/dist/src/lib/function-triggers.d.ts +3 -15
- package/dist/src/lib/function-triggers.js +10 -72
- package/dist/src/lib/function-triggers.js.map +1 -1
- package/dist/src/lib/generated-allowlist.js +0 -20
- package/dist/src/lib/generated-allowlist.js.map +1 -1
- package/dist/src/lib/generated-config-surfaces.d.ts +212 -49
- package/dist/src/lib/generated-config-surfaces.js +759 -184
- package/dist/src/lib/generated-config-surfaces.js.map +1 -1
- package/dist/src/lib/generated-sdk-types.d.ts +1 -1
- package/dist/src/lib/generated-sdk-types.js +1 -1
- package/dist/src/lib/generated-sdk-types.js.map +1 -1
- package/dist/src/lib/login-scope-lines.d.ts +15 -0
- package/dist/src/lib/login-scope-lines.js +22 -0
- package/dist/src/lib/login-scope-lines.js.map +1 -0
- package/dist/src/lib/paginate.d.ts +0 -5
- package/dist/src/lib/paginate.js +0 -5
- package/dist/src/lib/paginate.js.map +1 -1
- package/dist/src/lib/prompt-schema-codegen.js +5 -2
- package/dist/src/lib/prompt-schema-codegen.js.map +1 -1
- package/dist/src/lib/refresh-admin-credentials.d.ts +7 -0
- package/dist/src/lib/refresh-admin-credentials.js +16 -4
- package/dist/src/lib/refresh-admin-credentials.js.map +1 -1
- package/dist/src/lib/root-import-decision.d.ts +15 -14
- package/dist/src/lib/root-import-decision.js +44 -51
- package/dist/src/lib/root-import-decision.js.map +1 -1
- package/dist/src/lib/snapshot-build-rows.d.ts +21 -0
- package/dist/src/lib/snapshot-build-rows.js +22 -0
- package/dist/src/lib/snapshot-build-rows.js.map +1 -1
- package/dist/src/lib/snapshot-build.js +8 -3
- package/dist/src/lib/snapshot-build.js.map +1 -1
- package/dist/src/lib/swift-codegen/banners.d.ts +34 -7
- package/dist/src/lib/swift-codegen/banners.js +37 -7
- package/dist/src/lib/swift-codegen/banners.js.map +1 -1
- package/dist/src/lib/swift-codegen/dbGenerator.d.ts +4 -3
- package/dist/src/lib/swift-codegen/dbGenerator.js +14 -117
- package/dist/src/lib/swift-codegen/dbGenerator.js.map +1 -1
- package/dist/src/lib/swift-codegen/functionGenerator.d.ts +3 -3
- package/dist/src/lib/swift-codegen/functionGenerator.js +22 -17
- package/dist/src/lib/swift-codegen/functionGenerator.js.map +1 -1
- package/dist/src/lib/swift-codegen/generator.d.ts +5 -5
- package/dist/src/lib/swift-codegen/generator.js +35 -40
- package/dist/src/lib/swift-codegen/generator.js.map +1 -1
- package/dist/src/lib/toml-database-config.d.ts +14 -75
- package/dist/src/lib/toml-database-config.js +14 -305
- package/dist/src/lib/toml-database-config.js.map +1 -1
- package/dist/src/lib/workflow-codegen/generator.d.ts +7 -7
- package/dist/src/lib/workflow-codegen/generator.js +16 -23
- package/dist/src/lib/workflow-codegen/generator.js.map +1 -1
- package/dist/src/lib/workflow-codegen/invokerIR.d.ts +6 -12
- package/dist/src/lib/workflow-codegen/invokerIR.js +4 -5
- package/dist/src/lib/workflow-codegen/invokerIR.js.map +1 -1
- package/dist/src/lib/workflow-toml-validator.d.ts +2 -0
- package/dist/src/lib/workflow-toml-validator.js +28 -1
- package/dist/src/lib/workflow-toml-validator.js.map +1 -1
- package/package.json +3 -5
- package/dist/src/lib/generated-workflow-model-fields.d.ts +0 -20
- package/dist/src/lib/generated-workflow-model-fields.js +0 -53
- package/dist/src/lib/generated-workflow-model-fields.js.map +0 -1
- package/dist/src/lib/workflow-field-descriptor.d.ts +0 -91
- package/dist/src/lib/workflow-field-descriptor.js +0 -153
- package/dist/src/lib/workflow-field-descriptor.js.map +0 -1
|
@@ -2386,6 +2386,21 @@ export const RETIRED_CONFIG_KEYS = {
|
|
|
2386
2386
|
"`admin-only` (app admins; `personal-uploads` is the per-uploader " +
|
|
2387
2387
|
"preset).",
|
|
2388
2388
|
},
|
|
2389
|
+
// #4283 — #4116 made the bucket author its rule set by name and kept the
|
|
2390
|
+
// id spelling as a deprecated key. Its own sentence rather than the type
|
|
2391
|
+
// configs' shared one, which says a pulled file already spells the name:
|
|
2392
|
+
// a bucket file pulled before #4116 does not. The `request` half is never
|
|
2393
|
+
// surfaced, for the reason given at `RULE_SET_ID_RETIRED`.
|
|
2394
|
+
ruleSetId: {
|
|
2395
|
+
toml: "`ruleSetId` is no longer authored in TOML (#4283). A rule-set id " +
|
|
2396
|
+
"names a row in one app and resolves to nothing in another, so the " +
|
|
2397
|
+
"reference is authored by name: replace this line with " +
|
|
2398
|
+
'`ruleSetName = "<name>"`, where the name is the `name` the rule ' +
|
|
2399
|
+
"set's own `rule-sets/<key>.toml` declares, and push again, or " +
|
|
2400
|
+
"`config pull` rewrites the file.",
|
|
2401
|
+
request: "`ruleSetId` is the live request key on this surface; only its TOML " +
|
|
2402
|
+
"spelling is retired (#4283). Send the rule set's id.",
|
|
2403
|
+
},
|
|
2389
2404
|
},
|
|
2390
2405
|
// #2645 — pausing moved off `state` onto its own field, so `state` stopped
|
|
2391
2406
|
// being author-writable rather than becoming a second way to say "paused".
|
|
@@ -2423,9 +2438,46 @@ export const RETIRED_CONFIG_KEYS = {
|
|
|
2423
2438
|
},
|
|
2424
2439
|
},
|
|
2425
2440
|
// #3997 — keyed by each table's TOML prefix. Only `database-type-config`
|
|
2426
|
-
// uses `[type]`, so that entry cannot reach another surface
|
|
2427
|
-
// bucket's `[bucket] ruleSetId`
|
|
2428
|
-
|
|
2441
|
+
// uses `[type]`, so that entry cannot reach another surface. The blob
|
|
2442
|
+
// bucket's `[bucket] ruleSetId` has its own entry under `bucket` (#4283).
|
|
2443
|
+
//
|
|
2444
|
+
// #3189 — server-functions phase 6 retires database operations, and with
|
|
2445
|
+
// them the two type-level CEL keys that existed for operations: the access
|
|
2446
|
+
// an operation inherited and the gate on the (already retired, #3991)
|
|
2447
|
+
// database CEL context. Data access goes through a function, whose `access`
|
|
2448
|
+
// gate is the authorization; per-database values are resource metadata
|
|
2449
|
+
// categories. Refused rather than ignored: a file that keeps the line would
|
|
2450
|
+
// otherwise read as though it still gated something.
|
|
2451
|
+
type: {
|
|
2452
|
+
ruleSetId: RULE_SET_ID_RETIRED,
|
|
2453
|
+
defaultAccess: {
|
|
2454
|
+
toml: "`defaultAccess` is no longer a database type key (#3189): it was the " +
|
|
2455
|
+
"access database operations inherited, and operations are retired. " +
|
|
2456
|
+
"Read and write the type's records from a function — its `access` " +
|
|
2457
|
+
"gate is the authorization. Delete the line and push again.",
|
|
2458
|
+
request: "`defaultAccess` is retired (#3189): it was the access database " +
|
|
2459
|
+
"operations inherited, and operations are retired. Gate data access " +
|
|
2460
|
+
"with a function's `access` rule; omit the field.",
|
|
2461
|
+
},
|
|
2462
|
+
celContextAccess: {
|
|
2463
|
+
toml: "`celContextAccess` is no longer a database type key (#3189): the " +
|
|
2464
|
+
"database CEL context it gated is gone (#3991). Per-database values " +
|
|
2465
|
+
"are resource metadata categories, each with its own readRule and " +
|
|
2466
|
+
"writeRule. Delete the line and push again.",
|
|
2467
|
+
request: "`celContextAccess` is retired (#3189): the database CEL context it " +
|
|
2468
|
+
"gated is gone (#3991). Use a resource metadata category; omit the " +
|
|
2469
|
+
"field.",
|
|
2470
|
+
},
|
|
2471
|
+
metadataAccess: {
|
|
2472
|
+
toml: "`metadataAccess` is no longer a database type key (#3189): it was " +
|
|
2473
|
+
"the legacy spelling of `celContextAccess`, which gated the retired " +
|
|
2474
|
+
"database CEL context (#3991). Per-database values are resource " +
|
|
2475
|
+
"metadata categories. Delete the line and push again.",
|
|
2476
|
+
request: "`metadataAccess` is retired (#3189): the database CEL context it " +
|
|
2477
|
+
"gated is gone (#3991). Use a resource metadata category, which has " +
|
|
2478
|
+
"its own readRule and writeRule; omit the field.",
|
|
2479
|
+
},
|
|
2480
|
+
},
|
|
2429
2481
|
groupTypeConfig: { ruleSetId: RULE_SET_ID_RETIRED },
|
|
2430
2482
|
collectionTypeConfig: { ruleSetId: RULE_SET_ID_RETIRED },
|
|
2431
2483
|
// #2803 — the OBJECT-level availability leaves TOML. The per-version
|
|
@@ -2600,6 +2652,41 @@ export const RETIRED_CONFIG_KEYS = {
|
|
|
2600
2652
|
},
|
|
2601
2653
|
},
|
|
2602
2654
|
};
|
|
2655
|
+
/**
|
|
2656
|
+
* Top-level TOML tables a configuration object's FILE retired — keyed by the
|
|
2657
|
+
* surface's label, then by the table key. Only the TOML half exists: these were
|
|
2658
|
+
* sub-trees applied through their own endpoints, never request keys of the
|
|
2659
|
+
* object itself, so no request body can carry one.
|
|
2660
|
+
*
|
|
2661
|
+
* Without an entry here a retired table would be answered as an unrecognized
|
|
2662
|
+
* one ("check the spelling, or upgrade the CLI"), which is backwards for a
|
|
2663
|
+
* table removed on purpose.
|
|
2664
|
+
*/
|
|
2665
|
+
export const RETIRED_DOCUMENT_KEYS = {
|
|
2666
|
+
// #3189 — server-functions phase 6 retires database operations and
|
|
2667
|
+
// type-scoped subscriptions. Data access and change feeds are a function's
|
|
2668
|
+
// now; the rows the two tables created are swept at deploy.
|
|
2669
|
+
"database-type-config": {
|
|
2670
|
+
operations: {
|
|
2671
|
+
toml: "`[[operations]]` is no longer part of a database type file (#3189): " +
|
|
2672
|
+
"database operations are retired. Read and write the type's records " +
|
|
2673
|
+
"from a function (`ctx.db(<databaseId>).model(<Model>)`), and call the " +
|
|
2674
|
+
"function from the client. Delete every `[[operations]]` block and " +
|
|
2675
|
+
"push again.",
|
|
2676
|
+
},
|
|
2677
|
+
subscriptions: {
|
|
2678
|
+
toml: "`[[subscriptions]]` is no longer part of a database type file " +
|
|
2679
|
+
"(#3189): type-scoped subscriptions are retired. Publish changes on a " +
|
|
2680
|
+
"channel from the function that writes them " +
|
|
2681
|
+
"(`ctx.channels.authorize` / `ctx.channels.publish`). Delete every " +
|
|
2682
|
+
"`[[subscriptions]]` block and push again.",
|
|
2683
|
+
},
|
|
2684
|
+
},
|
|
2685
|
+
};
|
|
2686
|
+
/** The retired-table guidance for `key` on the surface labelled `label`, or null. */
|
|
2687
|
+
export function retiredDocumentKey(label, key) {
|
|
2688
|
+
return RETIRED_DOCUMENT_KEYS[label]?.[key] ?? null;
|
|
2689
|
+
}
|
|
2603
2690
|
/** The retired-key entry for `key` under `prefix`, or null when it is simply unknown. */
|
|
2604
2691
|
export function retiredConfigKey(prefix, key) {
|
|
2605
2692
|
return RETIRED_CONFIG_KEYS[prefix]?.[key] ?? null;
|
|
@@ -3336,7 +3423,8 @@ export function parseFunctionManifest(value) {
|
|
|
3336
3423
|
// ── src/config-surface/function-retired-keys.ts ──────────────────────────
|
|
3337
3424
|
/**
|
|
3338
3425
|
* The runtime keys a `functions/<key>.toml` no longer decides anything with —
|
|
3339
|
-
* #3482, project `server-functions` phase
|
|
3426
|
+
* #3482 retired them, and #3188 (project `server-functions` phase 5) refuses
|
|
3427
|
+
* them.
|
|
3340
3428
|
*
|
|
3341
3429
|
* The sponsor settled it on 2026-09-15 (the intent, "Mode vocabulary?",
|
|
3342
3430
|
* amended again): the config says NOTHING about how a function runs. A
|
|
@@ -3345,44 +3433,50 @@ export function parseFunctionManifest(value) {
|
|
|
3345
3433
|
* and a function that must not run under one runtime tests for it in code with
|
|
3346
3434
|
* `assertRuntime`.
|
|
3347
3435
|
*
|
|
3348
|
-
* ──
|
|
3436
|
+
* ── Accepted through phase 4, refused from phase 5 ────────────────────────
|
|
3349
3437
|
*
|
|
3350
|
-
* Every tree pushed since #3454 that declares a cron trigger
|
|
3351
|
-
* `mode = …` ON THE ENTRY, because `cron-trigger-mode-required` demanded it
|
|
3352
|
-
*
|
|
3353
|
-
*
|
|
3354
|
-
*
|
|
3355
|
-
*
|
|
3438
|
+
* Every tree pushed since #3454 that declares a cron trigger carried
|
|
3439
|
+
* `mode = …` ON THE ENTRY, because `cron-trigger-mode-required` demanded it,
|
|
3440
|
+
* so #3482 accepted the keys and warned rather than refusing them one day
|
|
3441
|
+
* after the platform insisted on them. Phase 5 is the declared breaking phase
|
|
3442
|
+
* that ends the grace: `mode`, `durable` and a cron entry's `mode` are refused
|
|
3443
|
+
* with one sentence per key, naming the key, saying the caller picks the
|
|
3444
|
+
* runtime, and saying to delete the line. `config pull` still strips them
|
|
3445
|
+
* (`stripRetiredFunctionKeys` in the CLI reads this scanner), so a tree pulled
|
|
3446
|
+
* from a version pushed before the retirement pushes again unchanged.
|
|
3356
3447
|
*
|
|
3357
|
-
*
|
|
3358
|
-
*
|
|
3359
|
-
*
|
|
3360
|
-
* nothing left to validate it against, and refusing a bad value for a key
|
|
3361
|
-
* nobody reads would be a refusal about spelling alone.
|
|
3448
|
+
* One rule id, one handler: `refusedFunctionKeys` is declared `preflight` on
|
|
3449
|
+
* the `function` surface and the CLI's push preflight runs it before the first
|
|
3450
|
+
* request. The value is not validated — the key's PRESENCE is the refusal.
|
|
3362
3451
|
*
|
|
3363
|
-
* Pure: no models, no tenant context, no `env` (see `types.ts` §Purity).
|
|
3364
|
-
*
|
|
3365
|
-
*
|
|
3366
|
-
*
|
|
3452
|
+
* Pure: no models, no tenant context, no `env` (see `types.ts` §Purity). The
|
|
3453
|
+
* CLI preflight reaches this module through the vendored copy
|
|
3454
|
+
* (`cli/scripts/gen-config-surfaces.mjs`), so the sentence an author reads from
|
|
3455
|
+
* `config push` is the one this module writes.
|
|
3367
3456
|
*/
|
|
3368
3457
|
/** The two `[function]` keys that no longer say anything (#3482). */
|
|
3369
3458
|
export const RETIRED_FUNCTION_RUNTIME_KEYS = ["mode", "durable"];
|
|
3459
|
+
/**
|
|
3460
|
+
* The rule ids this module owns, declared on the `function` surface (#3374's
|
|
3461
|
+
* anchor). One id covers the three spellings: they are one rule — the config
|
|
3462
|
+
* does not choose the runtime — met in three places.
|
|
3463
|
+
*/
|
|
3464
|
+
export const RETIRED_RUNTIME_KEY_RULE_IDS = ["retired-runtime-key"];
|
|
3465
|
+
/** The one refusal constructor (#3373's anchor). */
|
|
3466
|
+
function refuseRuntimeKey(ruleId, path, message) {
|
|
3467
|
+
return { ruleId, path, message };
|
|
3468
|
+
}
|
|
3370
3469
|
const RUNTIME_IS_THE_CALLERS = "a function is a function, and the caller picks the runtime at each call — " +
|
|
3371
3470
|
"`invoke` runs it inside the request, `start` runs it as a task";
|
|
3372
|
-
const FROM_PHASE_5 = "Remove the line; from phase 5 it is refused.";
|
|
3373
3471
|
/**
|
|
3374
|
-
* Every retired runtime key a `[function]` table
|
|
3472
|
+
* Every retired runtime key a `[function]` table carries.
|
|
3375
3473
|
*
|
|
3376
3474
|
* Reads the table the way the author wrote it: the two top-level keys, and a
|
|
3377
|
-
* `mode` on each `[[function.triggers.cron]]` entry.
|
|
3378
|
-
*
|
|
3379
|
-
*
|
|
3380
|
-
* gone).
|
|
3381
|
-
*
|
|
3382
|
-
* Order is the order an author reads their file in: the top-level keys first,
|
|
3383
|
-
* then the cron entries in declaration order.
|
|
3475
|
+
* `mode` on each `[[function.triggers.cron]]` entry. Order is the order an
|
|
3476
|
+
* author reads their file in: the top-level keys first, then the cron entries
|
|
3477
|
+
* in declaration order.
|
|
3384
3478
|
*/
|
|
3385
|
-
|
|
3479
|
+
function retiredKeySites(functionTable) {
|
|
3386
3480
|
const table = functionTable;
|
|
3387
3481
|
if (!table || typeof table !== "object" || Array.isArray(table))
|
|
3388
3482
|
return [];
|
|
@@ -3390,10 +3484,7 @@ export function ignoredFunctionKeys(functionTable) {
|
|
|
3390
3484
|
for (const key of RETIRED_FUNCTION_RUNTIME_KEYS) {
|
|
3391
3485
|
if (table[key] === undefined)
|
|
3392
3486
|
continue;
|
|
3393
|
-
found.push({
|
|
3394
|
-
path: key,
|
|
3395
|
-
message: `\`${key}\` is ignored (#3482): ${RUNTIME_IS_THE_CALLERS}. ${FROM_PHASE_5}`,
|
|
3396
|
-
});
|
|
3487
|
+
found.push({ path: key, cronEntry: null, key });
|
|
3397
3488
|
}
|
|
3398
3489
|
const triggers = table.triggers;
|
|
3399
3490
|
const rawCron = triggers && typeof triggers === "object" && !Array.isArray(triggers)
|
|
@@ -3411,19 +3502,38 @@ export function ignoredFunctionKeys(functionTable) {
|
|
|
3411
3502
|
return;
|
|
3412
3503
|
const rawName = entry.name;
|
|
3413
3504
|
const name = typeof rawName === "string" && rawName ? rawName : `#${index + 1}`;
|
|
3414
|
-
found.push({
|
|
3415
|
-
path: `triggers.cron.${name}.mode`,
|
|
3416
|
-
message: `\`mode\` on cron trigger '${name}' is ignored (#3482): every cron ` +
|
|
3417
|
-
`fire starts a task run, and ${RUNTIME_IS_THE_CALLERS}. ${FROM_PHASE_5}`,
|
|
3418
|
-
});
|
|
3505
|
+
found.push({ path: `triggers.cron.${name}.mode`, cronEntry: name, key: "mode" });
|
|
3419
3506
|
});
|
|
3420
3507
|
return found;
|
|
3421
3508
|
}
|
|
3509
|
+
/**
|
|
3510
|
+
* Every retired runtime key the table carries, each as the refusal the author
|
|
3511
|
+
* reads. A cron entry's sentence says the extra thing its author needs to hear
|
|
3512
|
+
* — that the schedule no longer chooses, because every fire starts a task run.
|
|
3513
|
+
*/
|
|
3514
|
+
export function refusedFunctionKeys(functionTable) {
|
|
3515
|
+
return retiredKeySites(functionTable).map((site) => refuseRuntimeKey("retired-runtime-key", site.path, site.cronEntry === null
|
|
3516
|
+
? `\`${site.key}\` is not a key of a function: ${RUNTIME_IS_THE_CALLERS}. ` +
|
|
3517
|
+
"Delete the line."
|
|
3518
|
+
: `\`mode\` on cron trigger '${site.cronEntry}' is not a key of a ` +
|
|
3519
|
+
`function: every cron fire starts a task run, and ${RUNTIME_IS_THE_CALLERS}. ` +
|
|
3520
|
+
"Delete the line."));
|
|
3521
|
+
}
|
|
3522
|
+
/**
|
|
3523
|
+
* The same refusals, rendered for one file — the shape `config push` and
|
|
3524
|
+
* `config diff` print.
|
|
3525
|
+
*
|
|
3526
|
+
* `label` is what the reader can act on: `functions/<key>.toml` from the CLI,
|
|
3527
|
+
* and the function's key from the API, which has no file to name.
|
|
3528
|
+
*/
|
|
3529
|
+
export function refusedFunctionKeyMessages(label, functionTable) {
|
|
3530
|
+
return refusedFunctionKeys(functionTable).map((refusal) => `${label}: ${refusal.message}`);
|
|
3531
|
+
}
|
|
3422
3532
|
/**
|
|
3423
3533
|
* The database types a `[function]` table's retired trigger block names —
|
|
3424
3534
|
* #3483.
|
|
3425
3535
|
*
|
|
3426
|
-
* Beside `
|
|
3536
|
+
* Beside `refusedFunctionKeys` because it answers the same question about the
|
|
3427
3537
|
* same table, for the same reader: `config pull` writes a version's AUTHORED
|
|
3428
3538
|
* bytes, and a version pushed before the removal carries the block. Without
|
|
3429
3539
|
* this the pulled tree would be refused on its next push — a tree an author
|
|
@@ -3484,16 +3594,6 @@ function retiredDatabaseTriggerValue(functionTable) {
|
|
|
3484
3594
|
}
|
|
3485
3595
|
return triggers.database;
|
|
3486
3596
|
}
|
|
3487
|
-
/**
|
|
3488
|
-
* The same scan, rendered for one file — the shape `config push` and
|
|
3489
|
-
* `config diff` print and the admin route answers with.
|
|
3490
|
-
*
|
|
3491
|
-
* `label` is what the reader can act on: `functions/<key>.toml` from the CLI,
|
|
3492
|
-
* and the function's key from the API, which has no file to name.
|
|
3493
|
-
*/
|
|
3494
|
-
export function ignoredFunctionKeyWarnings(label, functionTable) {
|
|
3495
|
-
return ignoredFunctionKeys(functionTable).map((ignored) => `${label}: ${ignored.message}`);
|
|
3496
|
-
}
|
|
3497
3597
|
export const EMPTY_TRIGGER_DECLARATION = {
|
|
3498
3598
|
webhook: null,
|
|
3499
3599
|
cron: [],
|
|
@@ -4194,7 +4294,10 @@ export function validateSigningSecretDeclaration(input) {
|
|
|
4194
4294
|
* `preflight`.
|
|
4195
4295
|
*/
|
|
4196
4296
|
/** The rule ids this module owns — the union type below anchors them (#3373). */
|
|
4197
|
-
export const WORKFLOW_RULE_IDS = [
|
|
4297
|
+
export const WORKFLOW_RULE_IDS = [
|
|
4298
|
+
"system-access-rule",
|
|
4299
|
+
"retired-database-step-kind",
|
|
4300
|
+
];
|
|
4198
4301
|
/** The one refusal constructor: a new branch cannot compile without an id. */
|
|
4199
4302
|
function refuseWorkflowRule(ruleId, message) {
|
|
4200
4303
|
return { ruleId, message };
|
|
@@ -4217,6 +4320,64 @@ export function checkWorkflowSystemAccessRule(input) {
|
|
|
4217
4320
|
}
|
|
4218
4321
|
return null;
|
|
4219
4322
|
}
|
|
4323
|
+
/**
|
|
4324
|
+
* The DSL step kinds that executed a registered database operation (#3189,
|
|
4325
|
+
* phase 6 of project `server-functions`, matrix row M8).
|
|
4326
|
+
*
|
|
4327
|
+
* Every `database.*` runner except `database.syncIndexes` called
|
|
4328
|
+
* `DatabaseOperationsController.execute`, so retiring database operations
|
|
4329
|
+
* retires these fourteen kinds with them: the seven base kinds and their seven
|
|
4330
|
+
* `*ForUser` twins. `database.syncIndexes` (which calls the records index
|
|
4331
|
+
* route), `database.create` and `database.delete` (resource lifecycle) are not
|
|
4332
|
+
* here and keep running until the engine retires.
|
|
4333
|
+
*/
|
|
4334
|
+
export const RETIRED_DATABASE_STEP_KINDS = [
|
|
4335
|
+
"database.query",
|
|
4336
|
+
"database.mutate",
|
|
4337
|
+
"database.count",
|
|
4338
|
+
"database.aggregate",
|
|
4339
|
+
"database.pipeline",
|
|
4340
|
+
"database.applyToQuery",
|
|
4341
|
+
"database.batch",
|
|
4342
|
+
"database.queryForUser",
|
|
4343
|
+
"database.mutateForUser",
|
|
4344
|
+
"database.countForUser",
|
|
4345
|
+
"database.aggregateForUser",
|
|
4346
|
+
"database.pipelineForUser",
|
|
4347
|
+
"database.applyToQueryForUser",
|
|
4348
|
+
"database.batchForUser",
|
|
4349
|
+
];
|
|
4350
|
+
/**
|
|
4351
|
+
* Every step in a definition — or the one step a run has reached — whose kind
|
|
4352
|
+
* is a retired database kind, with the sentence naming the replacement.
|
|
4353
|
+
*
|
|
4354
|
+
* Walks the top-level `steps` and a `collect` step's nested `step`, the one
|
|
4355
|
+
* place a step definition nests another. Push (the CLI preflight), save, the
|
|
4356
|
+
* activation lint and the engine all ask this one question, so a definition is
|
|
4357
|
+
* refused by the same words wherever it is caught, and a run that reaches such
|
|
4358
|
+
* a step fails with them.
|
|
4359
|
+
*/
|
|
4360
|
+
export function checkWorkflowRetiredStepKinds(input) {
|
|
4361
|
+
const violations = [];
|
|
4362
|
+
const visit = (step) => {
|
|
4363
|
+
if (!step || typeof step !== "object" || Array.isArray(step))
|
|
4364
|
+
return;
|
|
4365
|
+
const kind = step.kind;
|
|
4366
|
+
const id = step.id;
|
|
4367
|
+
if (typeof kind === "string" &&
|
|
4368
|
+
RETIRED_DATABASE_STEP_KINDS.includes(kind)) {
|
|
4369
|
+
const name = typeof id === "string" && id ? `step "${id}"` : "a step";
|
|
4370
|
+
violations.push(refuseWorkflowRule("retired-database-step-kind", `${name} uses ${kind}, which was retired with database operations — ` +
|
|
4371
|
+
"move its work into a server function (ctx.db(databaseId).model(name)) " +
|
|
4372
|
+
"and call that function from the workflow with a workflow.call step"));
|
|
4373
|
+
}
|
|
4374
|
+
if (kind === "collect")
|
|
4375
|
+
visit(step.step);
|
|
4376
|
+
};
|
|
4377
|
+
if (Array.isArray(input.steps))
|
|
4378
|
+
input.steps.forEach(visit);
|
|
4379
|
+
return violations;
|
|
4380
|
+
}
|
|
4220
4381
|
// ── src/config-surface/workflow.ts ───────────────────────────────────────
|
|
4221
4382
|
/**
|
|
4222
4383
|
* The `workflow` configuration object's definition (issue #2644, phase 1).
|
|
@@ -4339,6 +4500,10 @@ export const WORKFLOW_SURFACE = {
|
|
|
4339
4500
|
// Accepted on BOTH modes — #807's deferred second PATCH is an update
|
|
4340
4501
|
// call. The CLI's create-only send is a sequencing invariant owned by
|
|
4341
4502
|
// `applyWorkflowBody`, not a statement about what the server accepts.
|
|
4503
|
+
// #3188 retired the synchronous run route this flag opted into; the
|
|
4504
|
+
// field still round-trips (pull, diff, push) until the workflow
|
|
4505
|
+
// surface itself retires, and `config push` notes a `true` as
|
|
4506
|
+
// ignored. Its step-kind rule went with the route.
|
|
4342
4507
|
{
|
|
4343
4508
|
field: "syncCallable",
|
|
4344
4509
|
tomlKey: "syncCallable",
|
|
@@ -4563,8 +4728,9 @@ export const WORKFLOW_SURFACE = {
|
|
|
4563
4728
|
},
|
|
4564
4729
|
// ── Cross-field rules (#3374) ─────────────────────────────────────────────
|
|
4565
4730
|
//
|
|
4566
|
-
//
|
|
4567
|
-
// `workflow-rules.ts`
|
|
4731
|
+
// Two rules the CLI's push preflight runs — through the vendored copy of
|
|
4732
|
+
// `workflow-rules.ts` (the server runs both too) — and six more the server
|
|
4733
|
+
// runs at save time. What stays
|
|
4568
4734
|
// UNDECLARED, on purpose: the step-body validation (`validateWorkflowDefinition`
|
|
4569
4735
|
// and the `steps` structural sub-tree, which is not a field-table rule), and
|
|
4570
4736
|
// the enable-time activation lint (`workflow-activation-lint.ts`, a transition
|
|
@@ -4581,6 +4747,17 @@ export const WORKFLOW_SURFACE = {
|
|
|
4581
4747
|
stage: "preflight",
|
|
4582
4748
|
handler: enforcedBy("src/config-surface/workflow-rules.ts", "checkWorkflowSystemAccessRule"),
|
|
4583
4749
|
},
|
|
4750
|
+
{
|
|
4751
|
+
id: "retired-database-step-kind",
|
|
4752
|
+
description: "A step may not use a retired `database.*` kind (`database.query`, " +
|
|
4753
|
+
"`mutate`, `count`, `aggregate`, `pipeline`, `applyToQuery`, `batch` " +
|
|
4754
|
+
"and their `*ForUser` twins): each executed a registered database " +
|
|
4755
|
+
"operation, and operations retired (#3189). `database.syncIndexes`, " +
|
|
4756
|
+
"`database.create` and `database.delete` stay. Push, save, the " +
|
|
4757
|
+
"activation lint and the engine ask the same predicate.",
|
|
4758
|
+
stage: "preflight",
|
|
4759
|
+
handler: enforcedBy("src/config-surface/workflow-rules.ts", "checkWorkflowRetiredStepKinds"),
|
|
4760
|
+
},
|
|
4584
4761
|
workflowApplyRule("run-as-value", "validateWorkflowIdentityConfig", '`runAs` must be "caller" or "system" when present — the runner has no ' +
|
|
4585
4762
|
"third identity."),
|
|
4586
4763
|
workflowApplyRule("capabilities-shape", "validateWorkflowIdentityConfig", "`capabilities` must be an array or set of capability strings."),
|
|
@@ -4594,8 +4771,6 @@ export const WORKFLOW_SURFACE = {
|
|
|
4594
4771
|
workflowApplyRule("access-rule-required", "validateWorkflowIdentityConfig", "On create, a caller-mode workflow must state its `accessRule` (`\"true\"` " +
|
|
4595
4772
|
"for any app member), so a locked workflow is a decision rather than a " +
|
|
4596
4773
|
"default."),
|
|
4597
|
-
workflowApplyRule("sync-callable-step-kind", "validateSyncCallableSteps", "A `syncCallable = true` workflow may not carry a step kind that only " +
|
|
4598
|
-
"the durable runtime can execute (`delay`, `event.wait`)."),
|
|
4599
4774
|
],
|
|
4600
4775
|
};
|
|
4601
4776
|
const FIELD_RANGES = [
|
|
@@ -5697,6 +5872,9 @@ export const AGENT_ERROR_CODES = [
|
|
|
5697
5872
|
"AGENT_TOOL_ARGUMENTS_INVALID",
|
|
5698
5873
|
"AGENT_ROUND_TRUNCATED",
|
|
5699
5874
|
"AGENT_ROUND_EMPTY",
|
|
5875
|
+
// #4262 — a single-round test case whose seeded history, context or
|
|
5876
|
+
// assertions cannot be a round of this agent (`agent-test-case.ts`).
|
|
5877
|
+
"AGENT_TEST_CASE_INVALID",
|
|
5700
5878
|
];
|
|
5701
5879
|
/**
|
|
5702
5880
|
* The platform maximums. An agent may lower `maxSteps`, `waitLimitSeconds`
|
|
@@ -5721,6 +5899,20 @@ export const AGENT_NAME = /^[A-Za-z_][A-Za-z0-9_-]{0,63}$/;
|
|
|
5721
5899
|
export const AGENT_ANSWERED_BY = ["initiator", "participants"];
|
|
5722
5900
|
export const AGENT_TOOL_INVOKE = ["request", "task"];
|
|
5723
5901
|
export const AGENT_TOOL_HISTORY_SCOPES = ["session", "turn"];
|
|
5902
|
+
/**
|
|
5903
|
+
* The result the platform records for a call to a client tool with
|
|
5904
|
+
* `endsTurn = true`: the call is final when its response is committed, and
|
|
5905
|
+
* later rounds read this as its tool result.
|
|
5906
|
+
*/
|
|
5907
|
+
export const AGENT_TERMINAL_CALL_OUTPUT = Object.freeze({
|
|
5908
|
+
delivered: true,
|
|
5909
|
+
});
|
|
5910
|
+
/** The schema of `AGENT_TERMINAL_CALL_OUTPUT`: a terminal client tool's output type. */
|
|
5911
|
+
export const AGENT_TERMINAL_CALL_OUTPUT_SCHEMA = Object.freeze({
|
|
5912
|
+
type: "object",
|
|
5913
|
+
properties: { delivered: { type: "boolean" } },
|
|
5914
|
+
required: ["delivered"],
|
|
5915
|
+
});
|
|
5724
5916
|
const AGENT_TOP_LEVEL_KEYS = [
|
|
5725
5917
|
"answeredBy",
|
|
5726
5918
|
"maxSteps",
|
|
@@ -5742,6 +5934,7 @@ const AGENT_TOOL_KEYS = [
|
|
|
5742
5934
|
"approval",
|
|
5743
5935
|
"statusText",
|
|
5744
5936
|
"historyScope",
|
|
5937
|
+
"endsTurn",
|
|
5745
5938
|
"inputSchema",
|
|
5746
5939
|
"outputSchema",
|
|
5747
5940
|
];
|
|
@@ -5870,8 +6063,22 @@ function checkAgentTool(tool, index, names, out) {
|
|
|
5870
6063
|
out.push(agentInvalid(`${path}.invoke`, `must be one of ${AGENT_TOOL_INVOKE.join(" | ")}`));
|
|
5871
6064
|
}
|
|
5872
6065
|
}
|
|
6066
|
+
const terminal = tool.endsTurn === true;
|
|
6067
|
+
if (tool.endsTurn !== undefined && typeof tool.endsTurn !== "boolean") {
|
|
6068
|
+
out.push(agentInvalid(`${path}.endsTurn`, "must be true or false"));
|
|
6069
|
+
}
|
|
6070
|
+
if (terminal && tool.approval === true) {
|
|
6071
|
+
out.push(agentInvalid(`${path}.endsTurn`, "cannot be set with approval = true: a terminal call never pauses"));
|
|
6072
|
+
}
|
|
5873
6073
|
for (const key of ["inputSchema", "outputSchema"]) {
|
|
5874
6074
|
const schemaPath = `${path}.${key}`;
|
|
6075
|
+
if (isClient && terminal && key === "outputSchema") {
|
|
6076
|
+
if (tool[key] !== undefined) {
|
|
6077
|
+
out.push(agentInvalid(schemaPath, "is not declared on a client tool with endsTurn = true: the " +
|
|
6078
|
+
"platform records a terminal client call's result — remove it"));
|
|
6079
|
+
}
|
|
6080
|
+
continue;
|
|
6081
|
+
}
|
|
5875
6082
|
if (isClient) {
|
|
5876
6083
|
if (tool[key] === undefined) {
|
|
5877
6084
|
out.push(agentInvalid(schemaPath, "is required on a client tool"));
|
|
@@ -6146,17 +6353,14 @@ export function agentRoundNotAgentRefusal(promptKey) {
|
|
|
6146
6353
|
"agent rounds. Run it with ctx.prompts.run, or name an agent prompt");
|
|
6147
6354
|
}
|
|
6148
6355
|
/**
|
|
6149
|
-
* The refusal of a prompt test case that
|
|
6150
|
-
*
|
|
6151
|
-
*
|
|
6152
|
-
*
|
|
6356
|
+
* The refusal of a prompt test case that names an agent as its judge (§D1: an
|
|
6357
|
+
* evaluator runs single-shot, which an agent never does). A case written ON an
|
|
6358
|
+
* agent is a single-round test since #4262 (`agent-test-case.ts`), so
|
|
6359
|
+
* `evaluator` is the one role left.
|
|
6153
6360
|
*/
|
|
6154
|
-
export function agentTestCaseRefusal(promptKey,
|
|
6155
|
-
return agentRefusal("AGENT_TEST_CASE_NOT_SUPPORTED",
|
|
6156
|
-
|
|
6157
|
-
"cases are not supported for agents yet. Test its tools as functions"
|
|
6158
|
-
: `Prompt "${promptKey}" is an agent (kind = "agent") and cannot be an ` +
|
|
6159
|
-
"evaluator: an evaluator is run single-shot. Name a chat prompt");
|
|
6361
|
+
export function agentTestCaseRefusal(promptKey, _role = "evaluator") {
|
|
6362
|
+
return agentRefusal("AGENT_TEST_CASE_NOT_SUPPORTED", `Prompt "${promptKey}" is an agent (kind = "agent") and cannot be an ` +
|
|
6363
|
+
"evaluator: an evaluator is run single-shot. Name a chat prompt");
|
|
6160
6364
|
}
|
|
6161
6365
|
/** A deep copy, so a reader's defaults never reach the caller's value. */
|
|
6162
6366
|
function agentCopy(value) {
|
|
@@ -6165,7 +6369,7 @@ function agentCopy(value) {
|
|
|
6165
6369
|
/**
|
|
6166
6370
|
* The declaration with every default applied: `answeredBy = "initiator"`, the
|
|
6167
6371
|
* platform maximums for the limits, and per tool `invoke = "request"` (server
|
|
6168
|
-
* tools), `approval = false
|
|
6372
|
+
* tools), `approval = false`, `historyScope = "session"` and `endsTurn = false`.
|
|
6169
6373
|
*
|
|
6170
6374
|
* The ONLY accessor for defaults — the stored value never carries them
|
|
6171
6375
|
* (DSO-3798-001). Takes a value `resolveAgentDeclaration` accepted (or `null`)
|
|
@@ -6185,6 +6389,7 @@ export function normalizeAgentDeclaration(declaration) {
|
|
|
6185
6389
|
...(tool.runs === "client" ? {} : { invoke: tool.invoke ?? "request" }),
|
|
6186
6390
|
approval: tool.approval ?? false,
|
|
6187
6391
|
historyScope: tool.historyScope ?? "session",
|
|
6392
|
+
endsTurn: tool.endsTurn ?? false,
|
|
6188
6393
|
})),
|
|
6189
6394
|
events: source.events ?? [],
|
|
6190
6395
|
};
|
|
@@ -6902,6 +7107,365 @@ export function agentSchemaTranslationRefusals(input) {
|
|
|
6902
7107
|
}
|
|
6903
7108
|
return out;
|
|
6904
7109
|
}
|
|
7110
|
+
// ── src/config-surface/agent-test-case.ts ────────────────────────────────
|
|
7111
|
+
/**
|
|
7112
|
+
* What an agent test case may say — issue #4262 (single-round prompt tests
|
|
7113
|
+
* from a seeded history; design-docs/agents.md §D1).
|
|
7114
|
+
*
|
|
7115
|
+
* A test case on an agent prompt carries one `[test.agent]` block: a seeded
|
|
7116
|
+
* `history` (JSON text), the round's turn `context`, `timezone` and `locale`,
|
|
7117
|
+
* and two assertions on the round's calls (`expectedToolCall`,
|
|
7118
|
+
* `expectedNoToolCall`). On the wire and in storage those are six flat
|
|
7119
|
+
* `BlockTestCase` fields, `agentHistory` … `agentExpectedNoToolCall`.
|
|
7120
|
+
*
|
|
7121
|
+
* This module is the whole FILE-DECIDABLE rule for them: the history's shape,
|
|
7122
|
+
* its turns, its call and result pairing, where its events sit, the
|
|
7123
|
+
* assertions' shapes, and — when the caller passes the agent's declaration —
|
|
7124
|
+
* that the event kinds and the asserted tool are declared. The admin test-case
|
|
7125
|
+
* routes call it, and so does the CLI's push preflight through the vendored
|
|
7126
|
+
* artifact, so a refusal at push and a refusal at the route cannot disagree.
|
|
7127
|
+
* What needs app state or a schema validator (the input, context and event
|
|
7128
|
+
* schemas, the send route's zone and tag rules) is `agent-test-round.ts`'s.
|
|
7129
|
+
*
|
|
7130
|
+
* Every refusal carries `AGENT_TEST_CASE_INVALID` and the caller gets every
|
|
7131
|
+
* offending item at once, as the declaration resolver does.
|
|
7132
|
+
*
|
|
7133
|
+
* Pure and dependency-free apart from the agent declaration's types, like
|
|
7134
|
+
* every module under `src/config-surface/`.
|
|
7135
|
+
*/
|
|
7136
|
+
/** The six wire and stored fields, in the order the surface declares them. */
|
|
7137
|
+
export const AGENT_TEST_CASE_FIELDS = [
|
|
7138
|
+
"agentHistory",
|
|
7139
|
+
"agentContext",
|
|
7140
|
+
"agentTimezone",
|
|
7141
|
+
"agentLocale",
|
|
7142
|
+
"agentExpectedToolCall",
|
|
7143
|
+
"agentExpectedNoToolCall",
|
|
7144
|
+
];
|
|
7145
|
+
function agentTestRefusal(message) {
|
|
7146
|
+
return { code: "AGENT_TEST_CASE_INVALID", message: `[test.agent] ${message}` };
|
|
7147
|
+
}
|
|
7148
|
+
function agentTestIsObject(value) {
|
|
7149
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7150
|
+
}
|
|
7151
|
+
function agentTestIsText(value) {
|
|
7152
|
+
return typeof value === "string" && value.trim() !== "";
|
|
7153
|
+
}
|
|
7154
|
+
/** `null` and `undefined` are absence; so is `""` for a text field. */
|
|
7155
|
+
function agentTestIsUnset(value) {
|
|
7156
|
+
if (value === null || value === undefined)
|
|
7157
|
+
return true;
|
|
7158
|
+
return typeof value === "string" && value.trim() === "";
|
|
7159
|
+
}
|
|
7160
|
+
/** A JSON field: the value as given, or the JSON text a sidecar carries parsed. */
|
|
7161
|
+
function agentTestJsonValue(value) {
|
|
7162
|
+
if (typeof value !== "string")
|
|
7163
|
+
return { ok: true, value };
|
|
7164
|
+
try {
|
|
7165
|
+
return { ok: true, value: JSON.parse(value) };
|
|
7166
|
+
}
|
|
7167
|
+
catch {
|
|
7168
|
+
return { ok: false };
|
|
7169
|
+
}
|
|
7170
|
+
}
|
|
7171
|
+
/** The names a declaration declares, or `null` when the caller passed none. */
|
|
7172
|
+
function agentTestDeclaredNames(declaration, given) {
|
|
7173
|
+
if (!given)
|
|
7174
|
+
return null;
|
|
7175
|
+
let block = declaration;
|
|
7176
|
+
if (typeof block === "string") {
|
|
7177
|
+
try {
|
|
7178
|
+
block = JSON.parse(block);
|
|
7179
|
+
}
|
|
7180
|
+
catch {
|
|
7181
|
+
block = null;
|
|
7182
|
+
}
|
|
7183
|
+
}
|
|
7184
|
+
const names = (list) => new Set((Array.isArray(list) ? list : [])
|
|
7185
|
+
.map((item) => (agentTestIsObject(item) ? item.name : undefined))
|
|
7186
|
+
.filter((name) => typeof name === "string"));
|
|
7187
|
+
const source = agentTestIsObject(block) ? block : {};
|
|
7188
|
+
return { tools: names(source.tools), events: names(source.events) };
|
|
7189
|
+
}
|
|
7190
|
+
const AGENT_TEST_ENTRY_KEYS = {
|
|
7191
|
+
user: ["role", "text"],
|
|
7192
|
+
assistant: ["role", "text", "reasoning", "calls"],
|
|
7193
|
+
tool: ["role", "callId", "output", "error"],
|
|
7194
|
+
event: ["role", "kind", "refersTo", "data"],
|
|
7195
|
+
};
|
|
7196
|
+
/**
|
|
7197
|
+
* The history's entries and every refusal of their shape. A user entry
|
|
7198
|
+
* starts a turn and the last one's turn is the current one; every call is
|
|
7199
|
+
* answered exactly once, by a tool entry that follows its assistant entry;
|
|
7200
|
+
* an event precedes the current turn's user entry (a live event reaches the
|
|
7201
|
+
* model only in a later turn's history).
|
|
7202
|
+
*/
|
|
7203
|
+
function agentTestReadHistory(raw, names, out) {
|
|
7204
|
+
if (agentTestIsUnset(raw)) {
|
|
7205
|
+
out.push(agentTestRefusal("history is required on an agent case: a JSON array of user, assistant, tool and event entries, oldest first"));
|
|
7206
|
+
return null;
|
|
7207
|
+
}
|
|
7208
|
+
if (typeof raw !== "string") {
|
|
7209
|
+
out.push(agentTestRefusal("history must be JSON text (a JSON array written as one string)"));
|
|
7210
|
+
return null;
|
|
7211
|
+
}
|
|
7212
|
+
let parsed;
|
|
7213
|
+
try {
|
|
7214
|
+
parsed = JSON.parse(raw);
|
|
7215
|
+
}
|
|
7216
|
+
catch (error) {
|
|
7217
|
+
out.push(agentTestRefusal(`history is not valid JSON (${error?.message ?? "parse failed"})`));
|
|
7218
|
+
return null;
|
|
7219
|
+
}
|
|
7220
|
+
if (!Array.isArray(parsed)) {
|
|
7221
|
+
out.push(agentTestRefusal("history must be a JSON array of entries"));
|
|
7222
|
+
return null;
|
|
7223
|
+
}
|
|
7224
|
+
const entries = [];
|
|
7225
|
+
const calls = new Map();
|
|
7226
|
+
/** The calls of the last assistant entry, which tool entries may answer. */
|
|
7227
|
+
let open = new Set();
|
|
7228
|
+
let seenUser = false;
|
|
7229
|
+
const lastUser = parsed.reduce((at, entry, index) => (agentTestIsObject(entry) && entry.role === "user" ? index : at), -1);
|
|
7230
|
+
if (lastUser === -1) {
|
|
7231
|
+
out.push(agentTestRefusal("history needs at least one user entry; the last one is the round's current turn"));
|
|
7232
|
+
}
|
|
7233
|
+
parsed.forEach((entry, index) => {
|
|
7234
|
+
const at = `history[${index}]`;
|
|
7235
|
+
if (!agentTestIsObject(entry)) {
|
|
7236
|
+
out.push(agentTestRefusal(`${at} must be an object with a role`));
|
|
7237
|
+
return;
|
|
7238
|
+
}
|
|
7239
|
+
// An own-property read: a role such as "constructor" or "__proto__" must
|
|
7240
|
+
// not resolve to an inherited member of the table.
|
|
7241
|
+
const allowed = typeof entry.role === "string" && Object.prototype.hasOwnProperty.call(AGENT_TEST_ENTRY_KEYS, entry.role)
|
|
7242
|
+
? AGENT_TEST_ENTRY_KEYS[entry.role]
|
|
7243
|
+
: undefined;
|
|
7244
|
+
if (!allowed) {
|
|
7245
|
+
out.push(agentTestRefusal(`${at}.role must be "user", "assistant", "tool" or "event"; found ${JSON.stringify(entry.role)}`));
|
|
7246
|
+
return;
|
|
7247
|
+
}
|
|
7248
|
+
const unknown = Object.keys(entry).filter((key) => !allowed.includes(key));
|
|
7249
|
+
if (unknown.length > 0) {
|
|
7250
|
+
out.push(agentTestRefusal(`${at} has ${unknown.map((k) => `"${k}"`).join(", ")}, which a ${entry.role} entry does not carry; allowed: ${allowed.join(", ")}`));
|
|
7251
|
+
}
|
|
7252
|
+
if (entry.role === "user") {
|
|
7253
|
+
if (typeof entry.text !== "string" || entry.text === "") {
|
|
7254
|
+
out.push(agentTestRefusal(`${at}.text must be a non-empty string`));
|
|
7255
|
+
return;
|
|
7256
|
+
}
|
|
7257
|
+
open = new Set();
|
|
7258
|
+
seenUser = true;
|
|
7259
|
+
entries.push({ role: "user", text: entry.text });
|
|
7260
|
+
return;
|
|
7261
|
+
}
|
|
7262
|
+
if (entry.role === "assistant") {
|
|
7263
|
+
if (!seenUser) {
|
|
7264
|
+
out.push(agentTestRefusal(`${at} is an assistant entry before the first user entry; a turn starts with a user entry`));
|
|
7265
|
+
}
|
|
7266
|
+
const hasText = entry.text !== undefined;
|
|
7267
|
+
const hasCalls = entry.calls !== undefined;
|
|
7268
|
+
if (hasText && typeof entry.text !== "string") {
|
|
7269
|
+
out.push(agentTestRefusal(`${at}.text must be a string`));
|
|
7270
|
+
}
|
|
7271
|
+
if (entry.reasoning !== undefined && typeof entry.reasoning !== "string") {
|
|
7272
|
+
out.push(agentTestRefusal(`${at}.reasoning must be a string`));
|
|
7273
|
+
}
|
|
7274
|
+
const read = [];
|
|
7275
|
+
if (hasCalls) {
|
|
7276
|
+
if (!Array.isArray(entry.calls) || entry.calls.length === 0) {
|
|
7277
|
+
out.push(agentTestRefusal(`${at}.calls must be a non-empty array of { callId, name, arguments }`));
|
|
7278
|
+
}
|
|
7279
|
+
else {
|
|
7280
|
+
entry.calls.forEach((call, callIndex) => {
|
|
7281
|
+
const callAt = `${at}.calls[${callIndex}]`;
|
|
7282
|
+
if (!agentTestIsObject(call) ||
|
|
7283
|
+
!agentTestIsText(call.callId) ||
|
|
7284
|
+
!agentTestIsText(call.name) ||
|
|
7285
|
+
!("arguments" in call) ||
|
|
7286
|
+
Object.keys(call).some((key) => !["callId", "name", "arguments"].includes(key))) {
|
|
7287
|
+
out.push(agentTestRefusal(`${callAt} must be { callId, name, arguments }`));
|
|
7288
|
+
return;
|
|
7289
|
+
}
|
|
7290
|
+
if (calls.has(call.callId)) {
|
|
7291
|
+
out.push(agentTestRefusal(`${callAt}: call id "${call.callId}" is used twice; call ids are unique across the history`));
|
|
7292
|
+
return;
|
|
7293
|
+
}
|
|
7294
|
+
calls.set(call.callId, { answered: false });
|
|
7295
|
+
read.push({ callId: call.callId, name: call.name, arguments: call.arguments });
|
|
7296
|
+
});
|
|
7297
|
+
}
|
|
7298
|
+
}
|
|
7299
|
+
if (!(typeof entry.text === "string" && entry.text !== "") && read.length === 0 && !hasCalls) {
|
|
7300
|
+
out.push(agentTestRefusal(`${at} needs text or calls`));
|
|
7301
|
+
}
|
|
7302
|
+
open = new Set(read.map((call) => call.callId));
|
|
7303
|
+
entries.push({
|
|
7304
|
+
role: "assistant",
|
|
7305
|
+
...(typeof entry.text === "string" ? { text: entry.text } : {}),
|
|
7306
|
+
...(typeof entry.reasoning === "string" ? { reasoning: entry.reasoning } : {}),
|
|
7307
|
+
...(read.length > 0 ? { calls: read } : {}),
|
|
7308
|
+
});
|
|
7309
|
+
return;
|
|
7310
|
+
}
|
|
7311
|
+
if (entry.role === "tool") {
|
|
7312
|
+
if (!agentTestIsText(entry.callId)) {
|
|
7313
|
+
out.push(agentTestRefusal(`${at}.callId must name a call of an earlier assistant entry`));
|
|
7314
|
+
return;
|
|
7315
|
+
}
|
|
7316
|
+
const hasOutput = "output" in entry;
|
|
7317
|
+
const hasError = "error" in entry;
|
|
7318
|
+
if (hasOutput === hasError) {
|
|
7319
|
+
out.push(agentTestRefusal(`${at} must carry exactly one of output or error`));
|
|
7320
|
+
return;
|
|
7321
|
+
}
|
|
7322
|
+
const call = calls.get(entry.callId);
|
|
7323
|
+
if (!call) {
|
|
7324
|
+
out.push(agentTestRefusal(`${at} answers call "${entry.callId}", which no earlier assistant entry makes`));
|
|
7325
|
+
return;
|
|
7326
|
+
}
|
|
7327
|
+
if (call.answered) {
|
|
7328
|
+
out.push(agentTestRefusal(`${at} is a second result for call "${entry.callId}"; every call has exactly one`));
|
|
7329
|
+
return;
|
|
7330
|
+
}
|
|
7331
|
+
if (!open.has(entry.callId)) {
|
|
7332
|
+
out.push(agentTestRefusal(`${at} answers call "${entry.callId}", which is not a call of the assistant entry just before it; ` +
|
|
7333
|
+
"a result follows its call"));
|
|
7334
|
+
return;
|
|
7335
|
+
}
|
|
7336
|
+
call.answered = true;
|
|
7337
|
+
if (hasError) {
|
|
7338
|
+
const error = entry.error;
|
|
7339
|
+
if (!agentTestIsObject(error) || !agentTestIsText(error.code) || typeof error.message !== "string") {
|
|
7340
|
+
out.push(agentTestRefusal(`${at}.error must be { code, message }`));
|
|
7341
|
+
return;
|
|
7342
|
+
}
|
|
7343
|
+
entries.push({ role: "tool", callId: entry.callId, error: { code: error.code, message: error.message } });
|
|
7344
|
+
return;
|
|
7345
|
+
}
|
|
7346
|
+
entries.push({ role: "tool", callId: entry.callId, output: entry.output });
|
|
7347
|
+
return;
|
|
7348
|
+
}
|
|
7349
|
+
// An event.
|
|
7350
|
+
if (!agentTestIsText(entry.kind)) {
|
|
7351
|
+
out.push(agentTestRefusal(`${at}.kind must name an event kind the agent declares`));
|
|
7352
|
+
return;
|
|
7353
|
+
}
|
|
7354
|
+
if (names && !names.events.has(entry.kind)) {
|
|
7355
|
+
out.push(agentTestRefusal(`${at} has event kind "${entry.kind}", which the agent does not declare` +
|
|
7356
|
+
(names.events.size > 0 ? ` (declared: ${[...names.events].join(", ")})` : "")));
|
|
7357
|
+
}
|
|
7358
|
+
if (entry.refersTo !== undefined && (!agentTestIsText(entry.refersTo) || !calls.has(entry.refersTo))) {
|
|
7359
|
+
out.push(agentTestRefusal(`${at}.refersTo names call ${JSON.stringify(entry.refersTo)}, which no earlier assistant entry makes`));
|
|
7360
|
+
}
|
|
7361
|
+
if (lastUser !== -1 && index > lastUser) {
|
|
7362
|
+
out.push(agentTestRefusal(`${at} is an event after the current turn's user entry; a live event reaches the model only in a ` +
|
|
7363
|
+
"later turn, so place it before the last user entry"));
|
|
7364
|
+
}
|
|
7365
|
+
entries.push({
|
|
7366
|
+
role: "event",
|
|
7367
|
+
kind: entry.kind,
|
|
7368
|
+
...(typeof entry.refersTo === "string" ? { refersTo: entry.refersTo } : {}),
|
|
7369
|
+
data: entry.data ?? null,
|
|
7370
|
+
});
|
|
7371
|
+
});
|
|
7372
|
+
for (const [callId, call] of calls) {
|
|
7373
|
+
if (!call.answered) {
|
|
7374
|
+
out.push(agentTestRefusal(`call "${callId}" has no result; write one tool entry for it (an unanswered call as an error, ` +
|
|
7375
|
+
'e.g. { "code": "superseded", "message": "…" })'));
|
|
7376
|
+
}
|
|
7377
|
+
}
|
|
7378
|
+
const last = parsed[parsed.length - 1];
|
|
7379
|
+
if (agentTestIsObject(last) && last.role === "assistant") {
|
|
7380
|
+
out.push(agentTestRefusal("history ends with an assistant entry; a round starts from a user entry or a tool result"));
|
|
7381
|
+
}
|
|
7382
|
+
return entries;
|
|
7383
|
+
}
|
|
7384
|
+
function agentTestReadExpectedToolCall(raw, names, out) {
|
|
7385
|
+
if (agentTestIsUnset(raw))
|
|
7386
|
+
return null;
|
|
7387
|
+
const parsed = agentTestJsonValue(raw);
|
|
7388
|
+
const value = parsed.ok ? parsed.value : undefined;
|
|
7389
|
+
if (!agentTestIsObject(value) ||
|
|
7390
|
+
!agentTestIsText(value.name) ||
|
|
7391
|
+
Object.keys(value).some((key) => key !== "name" && key !== "arguments")) {
|
|
7392
|
+
out.push(agentTestRefusal("expectedToolCall must be { name, arguments? } (arguments is matched as a subset)"));
|
|
7393
|
+
return null;
|
|
7394
|
+
}
|
|
7395
|
+
if (names && !names.tools.has(value.name)) {
|
|
7396
|
+
out.push(agentTestRefusal(`expectedToolCall names tool "${value.name}", which the agent does not declare` +
|
|
7397
|
+
(names.tools.size > 0 ? ` (declared: ${[...names.tools].join(", ")})` : "")));
|
|
7398
|
+
}
|
|
7399
|
+
return { name: value.name, ...("arguments" in value ? { arguments: value.arguments } : {}) };
|
|
7400
|
+
}
|
|
7401
|
+
/**
|
|
7402
|
+
* The one rule: is this case's agent block something a round can be built
|
|
7403
|
+
* from? `{ ok: true, testCase }` carries it read (`null` on a case whose
|
|
7404
|
+
* prompt is not an agent). On a chat or decisions case, every agent key
|
|
7405
|
+
* holding a non-null value is refused by name; an explicit `null` is unset on
|
|
7406
|
+
* any case, which is how a full-state update clears one.
|
|
7407
|
+
*/
|
|
7408
|
+
export function resolveAgentTestCase(input) {
|
|
7409
|
+
const { fields } = input;
|
|
7410
|
+
const kind = agentTestIsUnset(input.kind) ? "chat" : String(input.kind).trim();
|
|
7411
|
+
const refusals = [];
|
|
7412
|
+
if (kind !== "agent") {
|
|
7413
|
+
const owner = kind === "chat" || kind === "decisions" ? `a ${kind} prompt` : `a ${kind}`;
|
|
7414
|
+
for (const key of AGENT_TEST_CASE_FIELDS) {
|
|
7415
|
+
const value = fields[key];
|
|
7416
|
+
if (value !== null && value !== undefined) {
|
|
7417
|
+
refusals.push(agentTestRefusal(`${key} is set on a case of ${owner}; only an agent prompt's case carries the agent block. ` +
|
|
7418
|
+
"Remove it, or send it as null"));
|
|
7419
|
+
}
|
|
7420
|
+
}
|
|
7421
|
+
return refusals.length > 0 ? { ok: false, refusals } : { ok: true, testCase: null };
|
|
7422
|
+
}
|
|
7423
|
+
const names = agentTestDeclaredNames(input.declaration, "declaration" in input);
|
|
7424
|
+
const history = agentTestReadHistory(fields.agentHistory, names, refusals);
|
|
7425
|
+
let context = null;
|
|
7426
|
+
if (!agentTestIsUnset(fields.agentContext)) {
|
|
7427
|
+
const parsed = agentTestJsonValue(fields.agentContext);
|
|
7428
|
+
if (parsed.ok && agentTestIsObject(parsed.value))
|
|
7429
|
+
context = parsed.value;
|
|
7430
|
+
else
|
|
7431
|
+
refusals.push(agentTestRefusal("context must be a JSON object (turn.* for the round)"));
|
|
7432
|
+
}
|
|
7433
|
+
const text = (key, label, example) => {
|
|
7434
|
+
const value = fields[key];
|
|
7435
|
+
if (value === null || value === undefined)
|
|
7436
|
+
return null;
|
|
7437
|
+
if (typeof value !== "string" || value === "") {
|
|
7438
|
+
refusals.push(agentTestRefusal(`${label} must be ${example}`));
|
|
7439
|
+
return null;
|
|
7440
|
+
}
|
|
7441
|
+
return value;
|
|
7442
|
+
};
|
|
7443
|
+
const timezone = text("agentTimezone", "timezone", 'an IANA time zone name such as "America/New_York"');
|
|
7444
|
+
const locale = text("agentLocale", "locale", 'a BCP 47 language tag such as "en-US"');
|
|
7445
|
+
const expectedToolCall = agentTestReadExpectedToolCall(fields.agentExpectedToolCall, names, refusals);
|
|
7446
|
+
let expectedNoToolCall = false;
|
|
7447
|
+
const noCall = fields.agentExpectedNoToolCall;
|
|
7448
|
+
if (noCall !== null && noCall !== undefined) {
|
|
7449
|
+
if (typeof noCall !== "boolean")
|
|
7450
|
+
refusals.push(agentTestRefusal("expectedNoToolCall must be true or false"));
|
|
7451
|
+
else
|
|
7452
|
+
expectedNoToolCall = noCall;
|
|
7453
|
+
}
|
|
7454
|
+
if (expectedToolCall && expectedNoToolCall) {
|
|
7455
|
+
refusals.push(agentTestRefusal("sets both expectedToolCall and expectedNoToolCall = true; a round cannot both call a tool and call none"));
|
|
7456
|
+
}
|
|
7457
|
+
const subset = fields.expectedJsonSubset;
|
|
7458
|
+
if (!agentTestIsUnset(subset) && !(agentTestIsObject(subset) && Object.keys(subset).length === 0) && subset !== "{}") {
|
|
7459
|
+
refusals.push(agentTestRefusal("expectedJsonSubset is not used on an agent case: an agent's answer is text, and anything structured is a " +
|
|
7460
|
+
"tool call. Assert it with expectedToolCall"));
|
|
7461
|
+
}
|
|
7462
|
+
if (refusals.length > 0 || !history)
|
|
7463
|
+
return { ok: false, refusals };
|
|
7464
|
+
return {
|
|
7465
|
+
ok: true,
|
|
7466
|
+
testCase: { history, context, timezone, locale, expectedToolCall, expectedNoToolCall },
|
|
7467
|
+
};
|
|
7468
|
+
}
|
|
6905
7469
|
// ── src/config-surface/prompt.ts ─────────────────────────────────────────
|
|
6906
7470
|
/**
|
|
6907
7471
|
* The `prompt` configuration object's definition (issue #2644, phase 2).
|
|
@@ -8246,8 +8810,8 @@ export const CRON_TRIGGER_SURFACE = {
|
|
|
8246
8810
|
* halves of the access model (#1020), and the only access keys: at most one may
|
|
8247
8811
|
* change per request, which is the controller's rule and stays there. The rule
|
|
8248
8812
|
* set is authored by name (`ruleSetName`) and sent as `ruleSetId`, as the type
|
|
8249
|
-
* configs do (#4116)
|
|
8250
|
-
* `accessPolicy` spelling
|
|
8813
|
+
* configs do (#4116). The id spelling in TOML (#4283) and the pre-#1020
|
|
8814
|
+
* `accessPolicy` spelling (#3996) are retired keys (`retired-keys.ts`).
|
|
8251
8815
|
*/
|
|
8252
8816
|
const BLOB_BUCKET_HANDLER = "src/app-api/controllers/blob-buckets-controller.ts";
|
|
8253
8817
|
export const BLOB_BUCKET_SURFACE = {
|
|
@@ -8303,7 +8867,7 @@ export const BLOB_BUCKET_SURFACE = {
|
|
|
8303
8867
|
// Authored as a name, sent as an id (#4116), so a tree copied into
|
|
8304
8868
|
// another app binds that app's rule set. Existence-checked against
|
|
8305
8869
|
// AccessRuleSet; mutually exclusive with a preset change in the same
|
|
8306
|
-
// request.
|
|
8870
|
+
// request. The TOML spelling of the id is a retired key (#4283).
|
|
8307
8871
|
validation: handledBy(BLOB_BUCKET_HANDLER, "BlobBucketsController", "apply"),
|
|
8308
8872
|
},
|
|
8309
8873
|
],
|
|
@@ -8353,14 +8917,6 @@ export const BLOB_BUCKET_SURFACE = {
|
|
|
8353
8917
|
note: "The bucket key. `config pull` writes it for readability; push takes " +
|
|
8354
8918
|
"the identity from the file name and attaches `bucketKey` itself.",
|
|
8355
8919
|
},
|
|
8356
|
-
ruleSetId: {
|
|
8357
|
-
kind: "deprecated",
|
|
8358
|
-
note: "The id spelling of the rule-set reference (#4116). An id names a " +
|
|
8359
|
-
"rule set in one app, so a copied tree cannot resolve it; " +
|
|
8360
|
-
"`ruleSetName` is the authored key. Still accepted on push, with " +
|
|
8361
|
-
"an advisory line, and `config pull` rewrites the file to " +
|
|
8362
|
-
"`ruleSetName`. Retiring it is a later change of its own.",
|
|
8363
|
-
},
|
|
8364
8920
|
},
|
|
8365
8921
|
responseOnlyKeys: {},
|
|
8366
8922
|
},
|
|
@@ -8541,20 +9097,21 @@ export const EMAIL_TEMPLATE_SURFACE = {
|
|
|
8541
9097
|
* The `database-type-config` configuration object's definition (issue #2644, phase 3).
|
|
8542
9098
|
*
|
|
8543
9099
|
* `database-type-configs/<databaseType>.toml` is the most structured file in the sync
|
|
8544
|
-
* slot: a `[type]` scalar table, plus
|
|
9100
|
+
* slot: a `[type]` scalar table, plus three authored sub-trees that keep the
|
|
8545
9101
|
* validators that already own them, per the spec's scope boundary —
|
|
8546
9102
|
*
|
|
8547
9103
|
* `[models.*]` → the stored `schema` TOML string (`parseSchemaToml`)
|
|
8548
|
-
* `[[operations]]` → `DatabaseTypeOperation` rows (`toml-params-validator.ts`)
|
|
8549
|
-
* `[[subscriptions]]` → `DatabaseTypeSubscription` rows (#803)
|
|
8550
9104
|
* `[triggers]` → the trigger-rule tree (`validateTriggerRules`)
|
|
8551
9105
|
* `[metadata]`/`secrets` → the declared-access manifest (#1304)
|
|
8552
9106
|
*
|
|
8553
|
-
* `
|
|
8554
|
-
*
|
|
8555
|
-
*
|
|
8556
|
-
*
|
|
8557
|
-
*
|
|
9107
|
+
* `schema`, `triggers` and `metadataManifest` are model fields and are
|
|
9108
|
+
* declared `structural` here, naming the channel and the validator, so their
|
|
9109
|
+
* absence from the `[type]` table reads as a decision rather than an omission.
|
|
9110
|
+
*
|
|
9111
|
+
* #3189 — `[[operations]]` and `[[subscriptions]]` retired with database
|
|
9112
|
+
* operations and type-scoped subscriptions; `config push` refuses a file that
|
|
9113
|
+
* still carries either, with the sentence `retired-keys.ts` states
|
|
9114
|
+
* (`RETIRED_DOCUMENT_KEYS`).
|
|
8558
9115
|
*
|
|
8559
9116
|
* The exposed `[type]` scalars are exactly the file-OWNED ones (#1567): `sync
|
|
8560
9117
|
* push` sends each as value-or-`null` so removing a line from the TOML clears
|
|
@@ -8593,30 +9150,6 @@ export const DATABASE_TYPE_SURFACE = {
|
|
|
8593
9150
|
// spelling is retired in TOML (#3997): `retired-keys.ts`, `type`.
|
|
8594
9151
|
validation: handledBy(DB_TYPE_HANDLER, "DatabaseTypeConfigController", "apply"),
|
|
8595
9152
|
},
|
|
8596
|
-
{
|
|
8597
|
-
field: "metadataAccess",
|
|
8598
|
-
tomlKey: "celContextAccess",
|
|
8599
|
-
type: "string",
|
|
8600
|
-
emit: "whenSet",
|
|
8601
|
-
writableOn: BOTH,
|
|
8602
|
-
// CEL, parsed before storing (#1336). The legacy `metadataAccess`
|
|
8603
|
-
// spelling stays readable on push — see tomlOnlyKeys.
|
|
8604
|
-
deprecated: {
|
|
8605
|
-
note: "one CEL expression gates both read and update of the database " +
|
|
8606
|
-
"CEL context; use a resource metadata category, which has " +
|
|
8607
|
-
"separate readRule and writeRule",
|
|
8608
|
-
},
|
|
8609
|
-
validation: handledBy(DB_TYPE_HANDLER, "DatabaseTypeConfigController", "apply"),
|
|
8610
|
-
},
|
|
8611
|
-
{
|
|
8612
|
-
field: "defaultAccess",
|
|
8613
|
-
tomlKey: "defaultAccess",
|
|
8614
|
-
type: "string",
|
|
8615
|
-
emit: "whenSet",
|
|
8616
|
-
writableOn: BOTH,
|
|
8617
|
-
// CEL, plus the shared `md`-shape allowlist (#1523).
|
|
8618
|
-
validation: handledBy(DB_TYPE_HANDLER, "DatabaseTypeConfigController", "apply"),
|
|
8619
|
-
},
|
|
8620
9153
|
{
|
|
8621
9154
|
field: "autoPopulatedFields",
|
|
8622
9155
|
tomlKey: "autoPopulatedFields",
|
|
@@ -8676,44 +9209,8 @@ export const DATABASE_TYPE_SURFACE = {
|
|
|
8676
9209
|
note: "User who created the config; assigned server-side.",
|
|
8677
9210
|
},
|
|
8678
9211
|
},
|
|
8679
|
-
tomlOnlyKeys: {
|
|
8680
|
-
|
|
8681
|
-
kind: "deprecated",
|
|
8682
|
-
note: "The legacy spelling of `celContextAccess`. Read on push when the " +
|
|
8683
|
-
"newer key is absent (#1567 keeps `celContextAccess = \"\"` an " +
|
|
8684
|
-
"explicit clear); pull writes `celContextAccess`.",
|
|
8685
|
-
},
|
|
8686
|
-
},
|
|
8687
|
-
requestOnlyKeys: {
|
|
8688
|
-
pendingOpDeletes: {
|
|
8689
|
-
note: "Operation names this push is deleting, sent with the type-config " +
|
|
8690
|
-
"PATCH so the server validates the remaining schema against the " +
|
|
8691
|
-
"operations that will survive rather than the current set.",
|
|
8692
|
-
},
|
|
8693
|
-
pendingOpUpdates: {
|
|
8694
|
-
note: "#1336 — the post-push form of the operations this push rewrites " +
|
|
8695
|
-
"({ name, access, params }), so the child-op re-lint evaluates " +
|
|
8696
|
-
"each against what the push is about to store rather than the " +
|
|
8697
|
-
"stale row. Read by `type-config-access-lint.ts`, the same " +
|
|
8698
|
-
"look-ahead contract as `pendingOpDeletes` / `finalOpNames`.",
|
|
8699
|
-
},
|
|
8700
|
-
pendingOpUpserts: {
|
|
8701
|
-
modes: ["update"],
|
|
8702
|
-
note: "#2732 — the full post-push BODY of every operation this push " +
|
|
8703
|
-
"lands ({ name, type, modelName, access, definition, params }), so " +
|
|
8704
|
-
"the schema-edit gate lints a rewritten operation as rewritten " +
|
|
8705
|
-
"rather than as stored. That is what makes removing a model and " +
|
|
8706
|
-
"rewriting the operations that named it a single push. Read by " +
|
|
8707
|
-
"`pending-op-set.ts`, which honors an entry only when it is a " +
|
|
8708
|
-
"whole operation body whose name `finalOpNames` keeps — anything " +
|
|
8709
|
-
"less is ignored and the stored operation is gated instead. A " +
|
|
8710
|
-
"create has no stored operations to gate.",
|
|
8711
|
-
},
|
|
8712
|
-
finalOpNames: {
|
|
8713
|
-
note: "The operation names the file will end with, for the same " +
|
|
8714
|
-
"look-ahead validation as `pendingOpDeletes`.",
|
|
8715
|
-
},
|
|
8716
|
-
},
|
|
9212
|
+
tomlOnlyKeys: {},
|
|
9213
|
+
requestOnlyKeys: {},
|
|
8717
9214
|
responseOnlyKeys: {},
|
|
8718
9215
|
},
|
|
8719
9216
|
],
|
|
@@ -8723,15 +9220,6 @@ export const DATABASE_TYPE_SURFACE = {
|
|
|
8723
9220
|
note: "#666 — the `[models.<Name>.fields.*]` blocks, re-serialized into the " +
|
|
8724
9221
|
"stored `schema` TOML string on push and merged back on pull.",
|
|
8725
9222
|
},
|
|
8726
|
-
operations: {
|
|
8727
|
-
kind: "structural",
|
|
8728
|
-
note: "The `[[operations]]` array. Keeps the validator that already owns it " +
|
|
8729
|
-
"(`cli/src/lib/toml-params-validator.ts`) and its own endpoints.",
|
|
8730
|
-
},
|
|
8731
|
-
subscriptions: {
|
|
8732
|
-
kind: "structural",
|
|
8733
|
-
note: "#803 — the `[[subscriptions]]` array, applied through its own endpoints.",
|
|
8734
|
-
},
|
|
8735
9223
|
triggers: {
|
|
8736
9224
|
kind: "structural",
|
|
8737
9225
|
note: "The type's authored trigger table, stored on the type config as " +
|
|
@@ -9219,7 +9707,7 @@ export const TRANSFORM_SURFACE = {
|
|
|
9219
9707
|
* config version (`ServerFunctionConfig`), but it is ONE table in the file: an
|
|
9220
9708
|
* author does not write a version, they write a function and push it. The
|
|
9221
9709
|
* definition therefore declares the header's field surface and lists the
|
|
9222
|
-
* version-carried keys (`entry`, `
|
|
9710
|
+
* version-carried keys (`entry`, `limits`) in `tomlOnlyKeys` beside
|
|
9223
9711
|
* `key` — they are authored, they are not fields of the header model, and they
|
|
9224
9712
|
* travel on the config-version route rather than in the header body. Declaring
|
|
9225
9713
|
* a second table over `ServerFunctionConfig` at the same `tomlPath` is not
|
|
@@ -9307,6 +9795,20 @@ const TRIGGER_DECLARATION_RULES = functionRules("preflight", "src/config-surface
|
|
|
9307
9795
|
["cron-trigger-expression-required", "A cron entry carries a `cron` expression."],
|
|
9308
9796
|
["cron-trigger-overlap-policy", "`overlapPolicy` is `skip` or `allow`."],
|
|
9309
9797
|
]);
|
|
9798
|
+
/**
|
|
9799
|
+
* The retired runtime keys (#3188). `mode`, `durable` and a cron entry's
|
|
9800
|
+
* `mode` were accepted with a warning through phase 4 (#3482); phase 5 refuses
|
|
9801
|
+
* them, first in the preflight's order, so an author reads them in the order
|
|
9802
|
+
* their file carries them.
|
|
9803
|
+
*/
|
|
9804
|
+
const RETIRED_RUNTIME_KEY_RULES = functionRules("preflight", "src/config-surface/function-retired-keys.ts", "refusedFunctionKeys", [
|
|
9805
|
+
[
|
|
9806
|
+
"retired-runtime-key",
|
|
9807
|
+
"`mode`, `durable` and a cron entry's `mode` are refused: the caller " +
|
|
9808
|
+
"picks the runtime at each call (`invoke` or `start`) and every cron " +
|
|
9809
|
+
"fire starts a task run, so the file has nothing to say. Delete the line.",
|
|
9810
|
+
],
|
|
9811
|
+
]);
|
|
9310
9812
|
/**
|
|
9311
9813
|
* The file-decidable half of the webhook `signingSecret` rule (#3320). The
|
|
9312
9814
|
* SAME handler under the SAME ids is declared `apply` on the standalone
|
|
@@ -9464,14 +9966,6 @@ export const SERVER_FUNCTION_SURFACE = {
|
|
|
9464
9966
|
note: "#3462 — WHICH admin put the active version live. Same two doors, " +
|
|
9465
9967
|
"same reason: the actor is observed, never declared.",
|
|
9466
9968
|
},
|
|
9467
|
-
runAs: {
|
|
9468
|
-
kind: "server-owned",
|
|
9469
|
-
note: "RETIRED from the write surface by #3279: function code acts as " +
|
|
9470
|
-
"the system whoever invoked it, so the mode the column chose " +
|
|
9471
|
-
"between no longer exists. Declared-and-deprecated storage only " +
|
|
9472
|
-
"until phase 6 drops the column; a stored value decides nothing " +
|
|
9473
|
-
"and is never echoed.",
|
|
9474
|
-
},
|
|
9475
9969
|
deleteLockedAt: {
|
|
9476
9970
|
kind: "server-owned",
|
|
9477
9971
|
note: "#3186 — the lease a hard delete takes before it scans for live " +
|
|
@@ -9508,24 +10002,21 @@ export const SERVER_FUNCTION_SURFACE = {
|
|
|
9508
10002
|
},
|
|
9509
10003
|
mode: {
|
|
9510
10004
|
kind: "structural",
|
|
9511
|
-
note: "RETIRED (#3482)
|
|
9512
|
-
"
|
|
9513
|
-
"
|
|
9514
|
-
"
|
|
9515
|
-
"
|
|
9516
|
-
"
|
|
9517
|
-
"
|
|
9518
|
-
"
|
|
9519
|
-
"
|
|
9520
|
-
"under one runtime says so in code with `assertRuntime`.",
|
|
10005
|
+
note: "RETIRED (#3482) and REFUSED from phase 5 (#3188) by the " +
|
|
10006
|
+
"`retired-runtime-key` rule, whose sentence names the key and " +
|
|
10007
|
+
"says to delete the line. A function is a function: the CALLER " +
|
|
10008
|
+
"picks the runtime at each call, with `invoke` running it inside " +
|
|
10009
|
+
"the request and `start` running it as a task, cron fires always " +
|
|
10010
|
+
"starting a task run and webhook deliveries always running as a " +
|
|
10011
|
+
"request. Listed here so the refusal is the rule's sentence rather " +
|
|
10012
|
+
"than the generic unknown-key one; `config pull` removes it from a " +
|
|
10013
|
+
"version pushed before the retirement.",
|
|
9521
10014
|
},
|
|
9522
10015
|
durable: {
|
|
9523
10016
|
kind: "structural",
|
|
9524
|
-
note: "RETIRED (#3482)
|
|
9525
|
-
"
|
|
9526
|
-
"function, and there is no such thing any more.
|
|
9527
|
-
"carries it pushes with a warning, `config pull` removes it, and " +
|
|
9528
|
-
"phase 5 refuses it.",
|
|
10017
|
+
note: "RETIRED (#3482) and REFUSED from phase 5 (#3188) — the same as " +
|
|
10018
|
+
"`mode`, whose alias it was, by the same rule. It named one runner " +
|
|
10019
|
+
"for the whole function, and there is no such thing any more.",
|
|
9529
10020
|
},
|
|
9530
10021
|
limits: {
|
|
9531
10022
|
kind: "structural",
|
|
@@ -9563,11 +10054,12 @@ export const SERVER_FUNCTION_SURFACE = {
|
|
|
9563
10054
|
tomlDocumentKeys: {},
|
|
9564
10055
|
// ── Cross-field rules (#3374) ─────────────────────────────────────────────
|
|
9565
10056
|
//
|
|
9566
|
-
// Single-field refusals inside these modules (`mode`
|
|
10057
|
+
// Single-field refusals inside these modules (a retired `mode` key, a webhook
|
|
9567
10058
|
// block's `verificationScheme`) are declared here too: this is the only
|
|
9568
10059
|
// id-bearing slot until #3375 gives `ConfigField.validation` a stage, and a
|
|
9569
10060
|
// module's list must equal its declarations in both directions.
|
|
9570
10061
|
crossFieldRules: [
|
|
10062
|
+
...RETIRED_RUNTIME_KEY_RULES,
|
|
9571
10063
|
...TRIGGER_DECLARATION_RULES,
|
|
9572
10064
|
...SIGNING_SECRET_RULES,
|
|
9573
10065
|
// #3376 — after the signing-secret group, mirroring the order
|
|
@@ -9601,8 +10093,23 @@ export const SERVER_FUNCTION_SURFACE = {
|
|
|
9601
10093
|
* WIRE shape is the parsed value. That asymmetry is the handler's, not the
|
|
9602
10094
|
* definition's: `parseTestCaseToml` parses on push and the admin API
|
|
9603
10095
|
* re-stringifies on store.
|
|
10096
|
+
*
|
|
10097
|
+
* #4262 — an agent prompt's case carries one more block, `[test.agent]`: the
|
|
10098
|
+
* seeded history its single round starts from, the round's turn context, zone
|
|
10099
|
+
* and tag, and two assertions on the round's calls. Six flat fields on the
|
|
10100
|
+
* wire, each `whenSet`, so a chat case's sidecar never grows an empty block.
|
|
10101
|
+
* `history` is typed `string` — JSON text in TOML, on the wire and in storage —
|
|
10102
|
+
* because `isEmptyForEmit` reads every array as empty for a `json` field, so a
|
|
10103
|
+
* `whenSet` array would never pull and would compare equal to anything.
|
|
9604
10104
|
*/
|
|
9605
10105
|
const TEST_CASE_HANDLER = "src/admin-api.ts";
|
|
10106
|
+
/**
|
|
10107
|
+
* #4262 — the agent block's one rule, vendored: the admin routes call it on
|
|
10108
|
+
* create and update, the CLI's preflight on an agent prompt's sidecar. Its
|
|
10109
|
+
* schema half (input, context and event schemas) needs the app's agent, so the
|
|
10110
|
+
* stage is `apply`.
|
|
10111
|
+
*/
|
|
10112
|
+
const AGENT_TEST_CASE_RULE = handledBy("src/config-surface/agent-test-case.ts", "resolveAgentTestCase", "apply");
|
|
9606
10113
|
/**
|
|
9607
10114
|
* ── Why this module's CLI handler is declared `apply` (#3375) ─────────────
|
|
9608
10115
|
*
|
|
@@ -9704,7 +10211,72 @@ export const TEST_CASE_SURFACE = {
|
|
|
9704
10211
|
// JSON object, deep-partial-matched against the parsed output.
|
|
9705
10212
|
validation: handledBy(CLI_SYNC, "parseTestCaseToml", "apply"),
|
|
9706
10213
|
},
|
|
10214
|
+
// #4262 — `[test.agent]`, an agent prompt's case only.
|
|
10215
|
+
{
|
|
10216
|
+
field: "agentHistory",
|
|
10217
|
+
tomlKey: "history",
|
|
10218
|
+
tomlGroup: "agent",
|
|
10219
|
+
// JSON text of the seeded history, stored as authored.
|
|
10220
|
+
type: "string",
|
|
10221
|
+
emit: "whenSet",
|
|
10222
|
+
writableOn: BOTH,
|
|
10223
|
+
validation: AGENT_TEST_CASE_RULE,
|
|
10224
|
+
},
|
|
10225
|
+
{
|
|
10226
|
+
field: "agentContext",
|
|
10227
|
+
tomlKey: "context",
|
|
10228
|
+
tomlGroup: "agent",
|
|
10229
|
+
// `turn.*` for the round; TOML carries the JSON text.
|
|
10230
|
+
type: "json",
|
|
10231
|
+
emit: "whenSet",
|
|
10232
|
+
writableOn: BOTH,
|
|
10233
|
+
validation: AGENT_TEST_CASE_RULE,
|
|
10234
|
+
},
|
|
10235
|
+
{
|
|
10236
|
+
field: "agentTimezone",
|
|
10237
|
+
tomlKey: "timezone",
|
|
10238
|
+
tomlGroup: "agent",
|
|
10239
|
+
type: "string",
|
|
10240
|
+
emit: "whenSet",
|
|
10241
|
+
writableOn: BOTH,
|
|
10242
|
+
validation: AGENT_TEST_CASE_RULE,
|
|
10243
|
+
},
|
|
10244
|
+
{
|
|
10245
|
+
field: "agentLocale",
|
|
10246
|
+
tomlKey: "locale",
|
|
10247
|
+
tomlGroup: "agent",
|
|
10248
|
+
type: "string",
|
|
10249
|
+
emit: "whenSet",
|
|
10250
|
+
writableOn: BOTH,
|
|
10251
|
+
validation: AGENT_TEST_CASE_RULE,
|
|
10252
|
+
},
|
|
10253
|
+
{
|
|
10254
|
+
field: "agentExpectedToolCall",
|
|
10255
|
+
tomlKey: "expectedToolCall",
|
|
10256
|
+
tomlGroup: "agent",
|
|
10257
|
+
// `{ name, arguments? }`; TOML carries the JSON text.
|
|
10258
|
+
type: "json",
|
|
10259
|
+
emit: "whenSet",
|
|
10260
|
+
writableOn: BOTH,
|
|
10261
|
+
validation: AGENT_TEST_CASE_RULE,
|
|
10262
|
+
},
|
|
10263
|
+
{
|
|
10264
|
+
field: "agentExpectedNoToolCall",
|
|
10265
|
+
tomlKey: "expectedNoToolCall",
|
|
10266
|
+
tomlGroup: "agent",
|
|
10267
|
+
type: "boolean",
|
|
10268
|
+
emit: "whenSet",
|
|
10269
|
+
writableOn: BOTH,
|
|
10270
|
+
validation: AGENT_TEST_CASE_RULE,
|
|
10271
|
+
},
|
|
9707
10272
|
],
|
|
10273
|
+
tomlGroups: {
|
|
10274
|
+
agent: {
|
|
10275
|
+
note: "#4262 — `[test.agent]`: the seeded history, turn context and " +
|
|
10276
|
+
"tool-call assertions of a single-round test on an agent prompt. " +
|
|
10277
|
+
"Refused on any other case.",
|
|
10278
|
+
},
|
|
10279
|
+
},
|
|
9708
10280
|
notExposed: {
|
|
9709
10281
|
testCaseId: {
|
|
9710
10282
|
kind: "server-owned",
|
|
@@ -10561,6 +11133,12 @@ export const GENERATED_CONFIG_MODEL_FIELDS = {
|
|
|
10561
11133
|
"evaluatorPromptId",
|
|
10562
11134
|
"evaluatorConfigId",
|
|
10563
11135
|
"attachments",
|
|
11136
|
+
"agentHistory",
|
|
11137
|
+
"agentContext",
|
|
11138
|
+
"agentTimezone",
|
|
11139
|
+
"agentLocale",
|
|
11140
|
+
"agentExpectedToolCall",
|
|
11141
|
+
"agentExpectedNoToolCall",
|
|
10564
11142
|
"createdBy",
|
|
10565
11143
|
"createdAt",
|
|
10566
11144
|
"modifiedAt",
|
|
@@ -10606,8 +11184,6 @@ export const GENERATED_CONFIG_MODEL_FIELDS = {
|
|
|
10606
11184
|
"databaseType",
|
|
10607
11185
|
"ruleSetId",
|
|
10608
11186
|
"triggers",
|
|
10609
|
-
"metadataAccess",
|
|
10610
|
-
"defaultAccess",
|
|
10611
11187
|
"metadataManifest",
|
|
10612
11188
|
"autoPopulatedFields",
|
|
10613
11189
|
"timestamps",
|
|
@@ -10663,7 +11239,6 @@ export const GENERATED_CONFIG_MODEL_FIELDS = {
|
|
|
10663
11239
|
"activeConfigId",
|
|
10664
11240
|
"activeConfigActivatedAt",
|
|
10665
11241
|
"activeConfigActivatedBy",
|
|
10666
|
-
"runAs",
|
|
10667
11242
|
"access",
|
|
10668
11243
|
"inputSchema",
|
|
10669
11244
|
"outputSchema",
|