@sema-agent/settings-schema 10.1.0 → 11.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +419 -0
- package/README.md +141 -65
- package/bin/settings-schema.mjs +13 -6
- package/conformance/VERSION +1 -1
- package/conformance/baseline.json +3 -1
- package/conformance/cases/c01-entry-salvage-then-typo/expected.json +3 -1
- package/conformance/cases/c02-hosts-clamp-stacked/expected.json +12 -5
- package/conformance/cases/c02-hosts-clamp-stacked/meta.json +1 -1
- package/conformance/cases/c03-literal-dotted-top-key/expected.json +22 -2
- package/conformance/cases/c04-all-bad-collection-not-salvaged/expected.json +3 -1
- package/conformance/cases/c06-legacy-governance-lift/expected.json +11 -101
- package/conformance/cases/c06-legacy-governance-lift/meta.json +1 -1
- package/conformance/cases/c07-hooks-is-not-a-config-domain/expected.json +3 -1
- package/conformance/cases/c08-limits-unknown-carried-64/expected.json +203 -2
- package/conformance/cases/c09-limits-unknown-carried-65/expected.json +204 -2
- package/conformance/cases/c10-record-key-secret-redacted-twice/expected.json +3 -1
- package/conformance/cases/c11-unicode-unknown-keys/expected.json +20 -2
- package/conformance/cases/c12-prototype-name-top-keys/expected.json +3 -1
- package/conformance/cases/c13-models-field-null/expected.json +3 -1
- package/conformance/cases/c14-models-field-empty-array/expected.json +6 -1
- package/conformance/cases/c15-models-field-number/expected.json +3 -1
- package/conformance/cases/c16-deep-nesting-205/expected.json +14 -2
- package/conformance/cases/c17-empty-domain-file/expected.json +3 -1
- package/conformance/cases/c18-wire-envelope-not-an-object/expected.json +3 -1
- package/conformance/cases/c19-wire-envelope-no-version/expected.json +3 -1
- package/conformance/cases/c20-governance-absent-lifts/expected.json +11 -86
- package/conformance/cases/c20-governance-absent-lifts/meta.json +1 -1
- package/conformance/cases/c21-governance-empty-object-wins/expected.json +6 -5
- package/conformance/cases/c21-governance-empty-object-wins/input.json +1 -4
- package/conformance/cases/c21-governance-empty-object-wins/meta.json +1 -1
- package/conformance/cases/c23-mixed-validity-models/expected.json +3 -1
- package/conformance/cases/c24-valid-entries-with-unknown-keys/expected.json +22 -2
- package/conformance/cases/c25-published-roundtrip-zero-warnings/expected.json +6 -1
- package/conformance/cases/c26-hosts-array-1024/expected.json +3 -1
- package/conformance/cases/c27-hosts-array-1025/expected.json +6 -7175
- package/conformance/cases/c27-hosts-array-1025/meta.json +1 -1
- package/conformance/cases/c28-large-document-over-1mb/expected.json +14 -2
- package/conformance/cases/c29-salvage-then-domain-refine-fails/expected.json +3 -1
- package/conformance/cases/c30-version-triple-a-first/expected.json +6 -1
- package/conformance/cases/c31-version-triple-b-changed/expected.json +6 -1
- package/conformance/cases/c32-version-triple-a-restored/expected.json +6 -1
- package/conformance/cases/c33-limits-nested-carried-key/expected.json +18 -2
- package/conformance/cases/c34-skills-content-vs-manifest/expected.json +19 -3
- package/conformance/cases/c35-portable-domain-file-is-null/expected.json +3 -1
- package/conformance/cases/c36-skills-entry-without-content/expected.json +6 -2
- package/conformance/cases/c37-skills-field-null/expected.json +6 -2
- package/conformance/cases/c38-remote-exec-is-a-non-domain-file/expected.json +3 -1
- package/conformance/cases/c39-hidden-file-in-config-d/expected.json +3 -1
- package/conformance/cases/c40-s712-identity-only-what-reads-out/expected.json +11 -4
- package/conformance/cases/c41-s792-encoded-pem-identity/expected.json +9 -3
- package/conformance/cases/c42-s792-unknown-key-secret-redacted/expected.json +39 -2
- package/conformance/cases/c43-s792-form-and-deep-encoded-pem/expected.json +4 -1
- package/conformance/cases/c44-s792-utf8-escape-glued-token/expected.json +3 -1
- package/conformance/cases/c45-s792-identity-bounds-and-decode-depth/expected.json +19 -10
- package/conformance/cases/c45-s792-identity-bounds-and-decode-depth/meta.json +1 -1
- package/conformance/cases/{c46-s811-host-name-url-userinfo-characterization → c46-s811-host-name-url-userinfo-withheld}/expected.json +12 -5
- package/conformance/cases/c46-s811-host-name-url-userinfo-withheld/meta.json +9 -0
- package/conformance/cases/{c47-s811-roles-key-url-userinfo-characterization → c47-s811-roles-key-url-userinfo-withheld}/expected.json +23 -3
- package/conformance/cases/{c47-s811-roles-key-url-userinfo-characterization → c47-s811-roles-key-url-userinfo-withheld}/input.json +6 -0
- package/conformance/cases/c47-s811-roles-key-url-userinfo-withheld/meta.json +10 -0
- package/conformance/cases/c48-s935-model-headers-transported/expected.json +6 -1
- package/conformance/cases/c49-s935-model-headers-secret-entry-dropped/expected.json +3 -1
- package/conformance/cases/c50-s784-lane-host-legal/expected.json +3 -1
- package/conformance/cases/c51-s784-lane-e2b-legal/expected.json +3 -1
- package/conformance/cases/c52-s784-lane-k8s-legal/expected.json +3 -1
- package/conformance/cases/c53-s784-lane-ssh-legal/expected.json +3 -1
- package/conformance/cases/c54-s784-lane-adb-legal/expected.json +3 -1
- package/conformance/cases/c55-s784-lane-local-docker-legal/expected.json +3 -1
- package/conformance/cases/c56-s784-lane-device-legal/expected.json +3 -1
- package/conformance/cases/c57-s784-g1-enabled-no-lane/expected.json +3 -1
- package/conformance/cases/c58-s784-g2-multitenant-host/expected.json +3 -1
- package/conformance/cases/c59-s784-g3-k8s-missing-image/expected.json +3 -1
- package/conformance/cases/c60-s784-g3-e2b-missing-key/expected.json +3 -1
- package/conformance/cases/c61-s784-g3-other-lane-key/expected.json +3 -1
- package/conformance/cases/c62-s784-g7a-session-tidb/expected.json +3 -1
- package/conformance/cases/c63-s784-host-two-replicas/expected.json +3 -1
- package/conformance/cases/c64-s784-multitenant-local-store/expected.json +3 -1
- package/conformance/cases/c65-s784-pg-without-db/expected.json +3 -1
- package/conformance/cases/c66-s784-memory-backend-mismatch/expected.json +3 -1
- package/conformance/cases/c67-s784-retired-field-loud/expected.json +3 -1
- package/conformance/cases/c67-s784-retired-field-loud/meta.json +1 -1
- package/conformance/cases/c68-s784-device-local-store/expected.json +3 -1
- package/conformance/cases/c69-deep-encoded-token-past-every-reading/expected.json +3 -1
- package/conformance/cases/c70-rewritten-token-in-front-of-encoded-pem-header/expected.json +3 -1
- package/conformance/cases/c71-one-scanner-write-reads-display-forms/expected.json +119 -0
- package/conformance/cases/c71-one-scanner-write-reads-display-forms/input.json +24 -0
- package/conformance/cases/c71-one-scanner-write-reads-display-forms/meta.json +10 -0
- package/conformance/cases/c72-dangling-reference-write-seat-refuses/expected.json +111 -0
- package/conformance/cases/c72-dangling-reference-write-seat-refuses/input.json +26 -0
- package/conformance/cases/c72-dangling-reference-write-seat-refuses/meta.json +6 -0
- package/conformance/cases/cc01-mcp-toolfaces-non-object/expected.json +3 -1
- package/conformance/cases/cc02-mcp-source-key-is-transported/expected.json +3 -1
- package/conformance/cases/cc03-model-compat-unknown-subkey/expected.json +6 -1
- package/conformance/cases/cli01-model-name-nfc-vs-nfd/expected.json +11 -1
- package/conformance/cases/cli02-control-and-zero-width-chars/expected.json +11 -1
- package/conformance/cases/cli03-credential-pasted-into-unknown-key/expected.json +16 -2
- package/conformance/cases/cli04-env-value-with-newline/expected.json +3 -1
- package/conformance/cases/cli05-long-and-prefix-colliding-names/expected.json +6 -1
- package/conformance/cases/t01-absent-domain-key-not-null/expected.json +3 -1
- package/conformance/cases/t03-prototype-name-key-not-swallowed/expected.json +29 -2
- package/conformance/cases/t04-secret-literal-never-in-warning-value/expected.json +3 -1
- package/conformance/cases/z01-every-domain-empty-object/expected.json +8 -4
- package/conformance/cases/z01-every-domain-empty-object/input.json +0 -1
- package/conformance/cases/z01-every-domain-empty-object/meta.json +1 -1
- package/conformance/cases/z02-every-domain-unknown-key/expected.json +163 -23
- package/conformance/cases/z02-every-domain-unknown-key/input.json +0 -3
- package/conformance/cases/z02-every-domain-unknown-key/meta.json +1 -1
- package/dist/api/auth-bridge.d.ts +51 -181
- package/dist/api/auth-bridge.js +58 -33
- package/dist/api/auth.d.ts +37 -146
- package/dist/api/auth.js +16 -26
- package/dist/api/feedback.d.ts +87 -0
- package/dist/api/feedback.js +66 -0
- package/dist/api/pull-token.d.ts +32 -0
- package/dist/api/pull-token.js +88 -0
- package/dist/api/scopes.d.ts +82 -174
- package/dist/api/scopes.js +23 -18
- package/dist/bundle.d.ts +15 -7
- package/dist/bundle.js +28 -27
- package/dist/cli.d.ts +3 -1
- package/dist/cli.js +132 -81
- package/dist/config-d-file.d.ts +14 -0
- package/dist/config-d-file.js +67 -0
- package/dist/config-fns.d.ts +87 -186
- package/dist/config-fns.js +207 -262
- package/dist/conformance/contract-adapter.d.ts +9 -2
- package/dist/conformance/contract-adapter.js +15 -7
- package/dist/conformance/corpus.d.ts +3 -0
- package/dist/conformance/corpus.js +5 -0
- package/dist/conformance/derive.d.ts +3 -0
- package/dist/conformance/index.d.ts +2 -2
- package/dist/conformance/index.js +2 -2
- package/dist/conformance/run.js +67 -22
- package/dist/conformance/types.d.ts +31 -15
- package/dist/conformance/types.js +5 -0
- package/dist/conformance/warnings.d.ts +1 -1
- package/dist/conformance/warnings.js +1 -4
- package/dist/cross-domain.d.ts +66 -22
- package/dist/cross-domain.js +164 -93
- package/dist/cross-session.d.ts +29 -11
- package/dist/default-model.d.ts +43 -0
- package/dist/default-model.js +34 -0
- package/dist/execution-lane-index.d.ts +4 -8
- package/dist/execution-lane-index.js +4 -8
- package/dist/execution-lanes.d.ts +31 -8
- package/dist/execution-lanes.js +23 -5
- package/dist/file-edit.js +5 -2
- package/dist/file-store.d.ts +7 -6
- package/dist/file-store.js +37 -50
- package/dist/fleet.d.ts +364 -762
- package/dist/fleet.js +277 -224
- package/dist/hooks.d.ts +1151 -4737
- package/dist/hooks.js +59 -15
- package/dist/index.d.ts +10 -4
- package/dist/index.js +13 -7
- package/dist/integer.d.ts +11 -0
- package/dist/integer.js +11 -0
- package/dist/issue-path.d.ts +10 -0
- package/dist/issue-path.js +10 -0
- package/dist/loopback.d.ts +26 -0
- package/dist/loopback.js +54 -0
- package/dist/migration-receipt.d.ts +42 -0
- package/dist/migration-receipt.js +12 -0
- package/dist/one-time-migrations.d.ts +56 -0
- package/dist/one-time-migrations.js +819 -0
- package/dist/prompts-digest.d.ts +1 -1
- package/dist/prompts-digest.js +3 -2
- package/dist/reader.d.ts +2 -2
- package/dist/remote-exec.d.ts +45 -187
- package/dist/remote-exec.js +13 -7
- package/dist/resolve-roster.d.ts +18 -3
- package/dist/resolve-roster.js +28 -25
- package/dist/retired-envs.d.ts +24 -0
- package/dist/retired-envs.js +114 -0
- package/dist/retired-form.d.ts +31 -0
- package/dist/retired-form.js +28 -0
- package/dist/safety-merge-spec.d.ts +131 -191
- package/dist/safety-merge-spec.js +18 -14
- package/dist/scheduler-lifetime.d.ts +5 -0
- package/dist/scheduler-lifetime.js +7 -0
- package/dist/scheduler-store-migration.d.ts +14 -0
- package/dist/scheduler-store-migration.js +14 -0
- package/dist/scheduler-store-node.d.ts +24 -9
- package/dist/scheduler-store-node.js +25 -15
- package/dist/scheduler-store.d.ts +47 -14
- package/dist/scheduler-store.js +32 -61
- package/dist/secret-refs.d.ts +3 -3
- package/dist/secret-refs.js +1 -1
- package/dist/secret-scan.d.ts +14 -24
- package/dist/secret-scan.js +45 -23
- package/dist/sha256-core.d.ts +16 -0
- package/dist/sha256-core.js +138 -0
- package/dist/sha256.d.ts +6 -1
- package/dist/sha256.js +8 -98
- package/dist/skills-manifest.js +1 -1
- package/dist/types.d.ts +1501 -13536
- package/dist/types.js +494 -438
- package/dist/url-shape.d.ts +34 -0
- package/dist/url-shape.js +61 -0
- package/dist/web-admin-credential.d.ts +96 -0
- package/dist/{center-credential.js → web-admin-credential.js} +46 -43
- package/dist/web-admin-env.d.ts +33 -0
- package/dist/web-admin-env.js +33 -0
- package/dist/worker-row-verdict.d.ts +16 -0
- package/dist/worker-row-verdict.js +31 -0
- package/dist/workers-rows.d.ts +34 -0
- package/dist/workers-rows.js +42 -0
- package/dist/zod-unwrap.d.ts +11 -0
- package/dist/zod-unwrap.js +25 -0
- package/package.json +17 -9
- package/conformance/cases/c46-s811-host-name-url-userinfo-characterization/meta.json +0 -6
- package/conformance/cases/c47-s811-roles-key-url-userinfo-characterization/meta.json +0 -6
- package/conformance/migrate-workers/m01-g5-k8s-env-lifted/expected.json +0 -49
- package/conformance/migrate-workers/m01-g5-k8s-env-lifted/input.json +0 -26
- package/conformance/migrate-workers/m01-g5-k8s-env-lifted/meta.json +0 -4
- package/conformance/migrate-workers/m02-g5-idempotent/expected.json +0 -38
- package/conformance/migrate-workers/m02-g5-idempotent/input.json +0 -33
- package/conformance/migrate-workers/m02-g5-idempotent/meta.json +0 -5
- package/conformance/migrate-workers/m03-g5-refused-no-lane/expected.json +0 -71
- package/conformance/migrate-workers/m03-g5-refused-no-lane/input.json +0 -42
- package/conformance/migrate-workers/m03-g5-refused-no-lane/meta.json +0 -4
- package/conformance/migrate-workers/m04-g7b-session-tidb-lifted/expected.json +0 -52
- package/conformance/migrate-workers/m04-g7b-session-tidb-lifted/input.json +0 -30
- package/conformance/migrate-workers/m04-g7b-session-tidb-lifted/meta.json +0 -4
- package/conformance/migrate-workers/m05-row-verdict-reserved-env/expected.json +0 -82
- package/conformance/migrate-workers/m05-row-verdict-reserved-env/input.json +0 -68
- package/conformance/migrate-workers/m05-row-verdict-reserved-env/meta.json +0 -4
- package/dist/center-credential.d.ts +0 -116
- package/dist/issue-text.d.ts +0 -10
- package/dist/issue-text.js +0 -8
- package/dist/migrate-workers.d.ts +0 -71
- package/dist/migrate-workers.js +0 -351
- package/dist/migrate.d.ts +0 -255
- package/dist/migrate.js +0 -542
- /package/conformance/cases/{c46-s811-host-name-url-userinfo-characterization → c46-s811-host-name-url-userinfo-withheld}/input.json +0 -0
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* #14 SAFETY MERGE-SPEC — the ONE machine-readable per-category merge/enforcement table that every settings
|
|
3
|
-
* resolver (TOC shell-adapter + TOB
|
|
3
|
+
* resolver (TOC shell-adapter + TOB web-admin/service) reads, so they never re-implement (and drift on) the merge
|
|
4
4
|
* law. This formalizes settings-core's v2-design §3 merge-spec into a published-data contract; the SEMANTIC
|
|
5
|
-
* author is settings-core,
|
|
5
|
+
* author is settings-core, web-admin maintains the formalized artifact here (this package), the axis VALUE domains
|
|
6
6
|
* are core's (@sema-agent/core SAFETY_AXIS_VOCABULARY — validated by a dev-only conformance test, never a runtime
|
|
7
|
-
* import). Each consumer keeps its OWN key→category map (client's SemaSettings KEY_CONTRACTS,
|
|
7
|
+
* import). Each consumer keeps its OWN key→category map (client's SemaSettings KEY_CONTRACTS, web-admin's
|
|
8
8
|
* config-field→category map); THIS file is only the shared category spec they all cite — no third copy of the law.
|
|
9
9
|
*
|
|
10
10
|
* Per-category behavior is fully determined by `mergeShape` + `minTrust` + `tighten` + `enforcement` + `fail` — and,
|
|
@@ -118,14 +118,16 @@ export const SpecEnforcement = z.enum(["gate", "server", "resolver", "client"]);
|
|
|
118
118
|
* enforcement on NON-secret keys (additive merges like mcpServers/rules/env); `gate`/`server`/secret → CLOSED. */
|
|
119
119
|
export const FailMode = z.enum(["closed", "open"]);
|
|
120
120
|
/** The monotone tighten-only direction for a category (which way is "stricter"). `enumRank.order` lists values
|
|
121
|
-
* from LOOSEST→STRICTEST
|
|
122
|
-
*
|
|
121
|
+
* from LOOSEST→STRICTEST — the category's whole vocabulary: a word outside it is refused, and a ONE-word order (11.0.0:
|
|
122
|
+
* `shellGate`, whose engine vocabulary is the single floor word) is the degenerate rank where the only merge is identity;
|
|
123
|
+
* `boolStricter.strict` is the bool value that is the tighter one; `setNarrower` = a subset is tighter; `policyDenyWins` =
|
|
124
|
+
* deny beats allow; `none` = not rank-tightenable (replace / conflict-reject). */
|
|
123
125
|
export const TightenRule = z.discriminatedUnion("rule", [
|
|
124
126
|
z.object({ rule: z.literal("none") }),
|
|
125
127
|
z.object({ rule: z.literal("policyDenyWins") }),
|
|
126
128
|
z.object({ rule: z.literal("setNarrower") }),
|
|
127
129
|
z.object({ rule: z.literal("boolStricter"), strict: z.boolean() }),
|
|
128
|
-
z.object({ rule: z.literal("enumRank"), order: z.array(z.string()).min(
|
|
130
|
+
z.object({ rule: z.literal("enumRank"), order: z.array(z.string()).min(1) }),
|
|
129
131
|
]);
|
|
130
132
|
export const MergeCategorySpec = z.object({
|
|
131
133
|
mergeShape: MergeShape,
|
|
@@ -158,15 +160,17 @@ export const SAFETY_MERGE_SPEC = {
|
|
|
158
160
|
ask: { direction: "tighten", minTrust: "local" },
|
|
159
161
|
},
|
|
160
162
|
},
|
|
161
|
-
//
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
//
|
|
163
|
+
// Order covers the FULL shell-addressable mode vocabulary — a word missing here is not "unknown", it makes every
|
|
164
|
+
// two-layer merge containing it hit the out-of-vocabulary reject arm (the whole bundle used to be dropped; shells now
|
|
165
|
+
// reject per-unit, but the semantic fix is a complete rank). Rank anchors (the engine's mode arbitration): bypass
|
|
166
|
+
// releases every releasable ask; auto hands them to the classifier, which may auto-release ANY benign ask (⊇
|
|
167
|
+
// acceptEdits' edits-only auto-allow); dontAsk = every would-ask → deny (allow rules still pass, tighter than default,
|
|
168
|
+
// looser than plan's handsReadOnly).
|
|
167
169
|
permissionMode: { mergeShape: "replace", minTrust: "global", tighten: { rule: "enumRank", order: ["bypassPermissions", "auto", "acceptEdits", "default", "dontAsk", "plan"] }, enforcement: "gate", fail: "closed", seam: "derive → toolPolicy / onAsk / handsReadOnly (§6.4); bypass = launch-flag only + MANAGED kill-switch" },
|
|
168
170
|
handsReadOnly: { mergeShape: "replace", minTrust: "global", tighten: { rule: "boolStricter", strict: true }, enforcement: "gate", fail: "closed", seam: "handsReadOnly" },
|
|
169
|
-
|
|
171
|
+
// The engine's shell floor has ONE word, `"always"` (the operator mandates a per-call confirmation for every shell command);
|
|
172
|
+
// the earlier `off` / `classify` are refused at the engine's door, so a layer carrying either is out of vocabulary here too.
|
|
173
|
+
shellGate: { mergeShape: "replace", minTrust: "global", tighten: { rule: "enumRank", order: ["always"] }, enforcement: "gate", fail: "closed", seam: "shellGate", coreAxis: "shellGate" },
|
|
170
174
|
approver: { mergeShape: "replace", minTrust: "global", tighten: { rule: "none" }, enforcement: "gate", fail: "closed", seam: "onAsk (conflict → reject)" },
|
|
171
175
|
// S-59 件八 — member-level admission for the sandbox category (the shell's temporary "which sub-key counts
|
|
172
176
|
// as tightening" table lifts upstream; its gate keeps only the VALUE-pole half as executing detail).
|
|
@@ -227,7 +231,7 @@ export const SAFETY_MERGE_SPEC = {
|
|
|
227
231
|
memoryScopes: { mergeShape: "concat", minTrust: "global", tighten: { rule: "none" }, enforcement: "client", fail: "open", seam: "MemoryStore" },
|
|
228
232
|
autoCompaction: { mergeShape: "replace", minTrust: "session", tighten: { rule: "none" }, enforcement: "client", fail: "open", seam: "compaction config" },
|
|
229
233
|
ui: { mergeShape: "replace", minTrust: "local", tighten: { rule: "none" }, enforcement: "client", fail: "open", seam: "shell-only (theme/keybindings/statusLine) — never touches the engine" },
|
|
230
|
-
org: { mergeShape: "replace", minTrust: "managed", tighten: { rule: "none" }, enforcement: "server", fail: "closed", seam: "
|
|
234
|
+
org: { mergeShape: "replace", minTrust: "managed", tighten: { rule: "none" }, enforcement: "server", fail: "closed", seam: "web-admin → governanceBaseline (SSO/SCIM/audit/budget, TOB-only)" },
|
|
231
235
|
};
|
|
232
236
|
/** Own-property table lookup. The category union is compile-time only; a JavaScript or deserialized caller can
|
|
233
237
|
* hand in `"__proto__"` / `"constructor"` / `"toString"`, which a bare index resolves through Object.prototype
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 包内私有(不进主桶):调度记录「是不是会话链上的记录」的**一只**读法 —— `lifetime === "session"`;其余(含缺席)一律是 durable。
|
|
3
|
+
* 读口的过期清理(`parseSchedulerStore`)与一次性迁移 SS-M3 的 durable 键重铸读同一只,两处对同一条记录不可能给出两种答案。
|
|
4
|
+
*/
|
|
5
|
+
export function isSessionLifetime(record) {
|
|
6
|
+
return record.lifetime === "session";
|
|
7
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 一次性迁移 **SS-M3**(调度店 durable 键)的改写步 —— 包内私有,由 `./one-time-migrations` 以 `migrateSchedulerRecords` 再导出;
|
|
3
|
+
* `parseSchedulerStore` 在读时调它(lazy-on-read,登记在迁移表里的唯一读时改写;窗关随迁移族删)。单拆成叶子是为了让轻量的
|
|
4
|
+
* `./scheduler-store` 契约不把整套域 schema 拉进来。
|
|
5
|
+
*
|
|
6
|
+
* 规则(= 改写前每次 parse 都在做的生效读法,零语义变):`lifetime` 不是 `"session"` 的行(缺席 = durable)按 durable 铸键
|
|
7
|
+
* `scope := principal ?? "default"`;`"session"` 行是会话链键,不碰。只认形:改写后的行判据自不满足 ⇒ 重跑恒等。
|
|
8
|
+
*/
|
|
9
|
+
import type { MigrationReceipt } from "./migration-receipt.js";
|
|
10
|
+
import type { SchedulerRecord } from "./scheduler-store.js";
|
|
11
|
+
export declare function migrateSchedulerRecords(records: readonly SchedulerRecord[]): {
|
|
12
|
+
records: SchedulerRecord[];
|
|
13
|
+
receipts: MigrationReceipt[];
|
|
14
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { isSessionLifetime } from "./scheduler-lifetime.js";
|
|
2
|
+
export function migrateSchedulerRecords(records) {
|
|
3
|
+
const receipts = [];
|
|
4
|
+
const out = records.map((r, index) => {
|
|
5
|
+
if (isSessionLifetime(r))
|
|
6
|
+
return r;
|
|
7
|
+
const want = r.principal ?? "default";
|
|
8
|
+
if (r.scope === want)
|
|
9
|
+
return r;
|
|
10
|
+
receipts.push({ migration: "SS-M3", document: "scheduler-store", path: [index, "scope"], appliedAs: "rewritten" });
|
|
11
|
+
return { ...r, scope: want };
|
|
12
|
+
});
|
|
13
|
+
return { records: out, receipts };
|
|
14
|
+
}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { type SchedulerRecord, type SchedulerStore } from './scheduler-store.js';
|
|
1
|
+
import { type SchedulerParseWarning, type SchedulerReadOptions, type SchedulerRecord, type SchedulerStore, type SchedulerStoreRead } from './scheduler-store.js';
|
|
2
2
|
/**
|
|
3
|
-
* Load the durable store. A MISSING file (ENOENT) ⇒
|
|
3
|
+
* Load the durable store. A MISSING file (ENOENT) ⇒ no records, no warnings (first-run/never-written case); any OTHER
|
|
4
4
|
* read failure (EACCES/EIO/EISDIR/…) RETHROWS — only absence means "empty", not "unreadable for any reason"
|
|
5
5
|
* (a permissions/IO fault silently treated as [] would let a caller-side save() overwrite real on-disk data with
|
|
6
|
-
* an empty store).
|
|
7
|
-
*
|
|
6
|
+
* an empty store). Content the parser could not read verbatim comes back in `warnings` ({@link parseSchedulerStore});
|
|
7
|
+
* a whole file that is not a JSON array reads as zero records + `unreadable-file` — NOT as an empty store (writers must go
|
|
8
|
+
* through {@link mutateSchedulerStore}, which refuses to write over it).
|
|
8
9
|
*/
|
|
9
|
-
export declare function loadSchedulerStore(path: string):
|
|
10
|
+
export declare function loadSchedulerStore(path: string, opts: SchedulerReadOptions): SchedulerStoreRead;
|
|
10
11
|
/**
|
|
11
12
|
* Persist ATOMICALLY: `mkdir -p` the parent, write a uniquely-named `.tmp` sibling (mode 0o600 — owner-only, the
|
|
12
13
|
* records may carry principal/session identifiers), then `rename` over the target (atomic on POSIX, so a
|
|
@@ -19,10 +20,14 @@ export declare function saveSchedulerStore(path: string, records: SchedulerRecor
|
|
|
19
20
|
* 的**活对象** ⇒ 按本组工厂命名法(`build*` 出纯数据、`create*` 出活对象)该叫 `create*`;2.0.0 那次照人眼
|
|
20
21
|
* 改只改到了 `createSiblingResolver`,漏了这处 —— 现在由 `test/factory-naming-4.0.0.test.ts` 的门④ 机器看着。
|
|
21
22
|
* 消费方改调 `createFileSchedulerStore`(改名是硬 breaking:消费方的 tsc 红 / 运行期 undefined 就是通知本身)。 */
|
|
22
|
-
export declare function createFileSchedulerStore(path: string
|
|
23
|
+
export declare function createFileSchedulerStore(path: string, opts: {
|
|
24
|
+
now: () => number;
|
|
25
|
+
}): SchedulerStore;
|
|
23
26
|
/** Outcome of {@link mutateSchedulerStore}. `conflict_exhausted` = a concurrent writer kept changing the file
|
|
24
|
-
* faster than the bounded replays; nothing was written on that final attempt (earlier attempts never wrote).
|
|
25
|
-
|
|
27
|
+
* faster than the bounded replays; nothing was written on that final attempt (earlier attempts never wrote).
|
|
28
|
+
* `unreadable_file` = the file on disk is not a JSON array: `fn` never ran and nothing was written (writing would replace
|
|
29
|
+
* data this reader cannot read with `fn`'s output). Every outcome carries the `warnings` of the last base read. */
|
|
30
|
+
export type SchedulerStoreMutation = ({
|
|
26
31
|
ok: true;
|
|
27
32
|
records: SchedulerRecord[];
|
|
28
33
|
retries: number;
|
|
@@ -33,6 +38,11 @@ export type SchedulerStoreMutation = {
|
|
|
33
38
|
} | {
|
|
34
39
|
ok: false;
|
|
35
40
|
reason: 'aborted';
|
|
41
|
+
} | {
|
|
42
|
+
ok: false;
|
|
43
|
+
reason: 'unreadable_file';
|
|
44
|
+
}) & {
|
|
45
|
+
warnings: SchedulerParseWarning[];
|
|
36
46
|
};
|
|
37
47
|
/**
|
|
38
48
|
* The ONE cross-process mutation primitive for the shared scheduler store ([ref]①/② —
|
|
@@ -54,7 +64,12 @@ export type SchedulerStoreMutation = {
|
|
|
54
64
|
* eliminate it (that would need an advisory lock protocol every writer joins; rejected for
|
|
55
65
|
* stale-lock ops burden + win32 friction). `fn` re-runs on replay: side effects inside `fn` are a
|
|
56
66
|
* caller bug.
|
|
67
|
+
*
|
|
68
|
+
* 11.0.0: this is the ONE write primitive every writer goes through (the naked load→save sequence is the lost-update race
|
|
69
|
+
* above). A base file that is not a JSON array ⇒ `unreadable_file`, no write. `nowMs` is the caller's clock (the read drops
|
|
70
|
+
* session-pinned records that went stale — see `parseSchedulerStore`).
|
|
57
71
|
*/
|
|
58
|
-
export declare function mutateSchedulerStore(path: string, fn: (records: SchedulerRecord[]) => SchedulerRecord[] | null, opts
|
|
72
|
+
export declare function mutateSchedulerStore(path: string, fn: (records: SchedulerRecord[]) => SchedulerRecord[] | null, opts: {
|
|
73
|
+
nowMs: number;
|
|
59
74
|
retries?: number;
|
|
60
75
|
}): SchedulerStoreMutation;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @sema-agent/settings-schema/node — the Node filesystem BINDING of the scheduler-store contract. Split out of the
|
|
3
|
-
* pure barrel (it pulls `node:fs`/`node:path`/`node:crypto`) per
|
|
3
|
+
* pure barrel (it pulls `node:fs`/`node:path`/`node:crypto`) per this package's browser-safe discipline. Both
|
|
4
4
|
* the server-side writer and the TOC shell daemon import THIS for the on-disk store; the FORMAT lives in the
|
|
5
5
|
* pure `./scheduler-store` contract, so there is one shape and no drift.
|
|
6
6
|
*
|
|
@@ -11,21 +11,24 @@ import { dirname } from 'node:path';
|
|
|
11
11
|
import { randomBytes } from 'node:crypto';
|
|
12
12
|
import { parseSchedulerStore, serializeSchedulerStore } from './scheduler-store.js';
|
|
13
13
|
/**
|
|
14
|
-
* Load the durable store. A MISSING file (ENOENT) ⇒
|
|
14
|
+
* Load the durable store. A MISSING file (ENOENT) ⇒ no records, no warnings (first-run/never-written case); any OTHER
|
|
15
15
|
* read failure (EACCES/EIO/EISDIR/…) RETHROWS — only absence means "empty", not "unreadable for any reason"
|
|
16
16
|
* (a permissions/IO fault silently treated as [] would let a caller-side save() overwrite real on-disk data with
|
|
17
|
-
* an empty store).
|
|
18
|
-
*
|
|
17
|
+
* an empty store). Content the parser could not read verbatim comes back in `warnings` ({@link parseSchedulerStore});
|
|
18
|
+
* a whole file that is not a JSON array reads as zero records + `unreadable-file` — NOT as an empty store (writers must go
|
|
19
|
+
* through {@link mutateSchedulerStore}, which refuses to write over it).
|
|
19
20
|
*/
|
|
20
|
-
export function loadSchedulerStore(path) {
|
|
21
|
+
export function loadSchedulerStore(path, opts) {
|
|
22
|
+
let text;
|
|
21
23
|
try {
|
|
22
|
-
|
|
24
|
+
text = readFileSync(path, 'utf-8');
|
|
23
25
|
}
|
|
24
26
|
catch (e) {
|
|
25
27
|
if (e.code === 'ENOENT')
|
|
26
|
-
return [];
|
|
28
|
+
return { records: [], warnings: [] };
|
|
27
29
|
throw e;
|
|
28
30
|
}
|
|
31
|
+
return parseSchedulerStore(text, opts);
|
|
29
32
|
}
|
|
30
33
|
/**
|
|
31
34
|
* Persist ATOMICALLY: `mkdir -p` the parent, write a uniquely-named `.tmp` sibling (mode 0o600 — owner-only, the
|
|
@@ -44,9 +47,9 @@ export function saveSchedulerStore(path, records) {
|
|
|
44
47
|
* 的**活对象** ⇒ 按本组工厂命名法(`build*` 出纯数据、`create*` 出活对象)该叫 `create*`;2.0.0 那次照人眼
|
|
45
48
|
* 改只改到了 `createSiblingResolver`,漏了这处 —— 现在由 `test/factory-naming-4.0.0.test.ts` 的门④ 机器看着。
|
|
46
49
|
* 消费方改调 `createFileSchedulerStore`(改名是硬 breaking:消费方的 tsc 红 / 运行期 undefined 就是通知本身)。 */
|
|
47
|
-
export function createFileSchedulerStore(path) {
|
|
50
|
+
export function createFileSchedulerStore(path, opts) {
|
|
48
51
|
return {
|
|
49
|
-
load: () => loadSchedulerStore(path),
|
|
52
|
+
load: () => loadSchedulerStore(path, { nowMs: opts.now() }),
|
|
50
53
|
save: (records) => saveSchedulerStore(path, records),
|
|
51
54
|
};
|
|
52
55
|
}
|
|
@@ -70,9 +73,13 @@ export function createFileSchedulerStore(path) {
|
|
|
70
73
|
* eliminate it (that would need an advisory lock protocol every writer joins; rejected for
|
|
71
74
|
* stale-lock ops burden + win32 friction). `fn` re-runs on replay: side effects inside `fn` are a
|
|
72
75
|
* caller bug.
|
|
76
|
+
*
|
|
77
|
+
* 11.0.0: this is the ONE write primitive every writer goes through (the naked load→save sequence is the lost-update race
|
|
78
|
+
* above). A base file that is not a JSON array ⇒ `unreadable_file`, no write. `nowMs` is the caller's clock (the read drops
|
|
79
|
+
* session-pinned records that went stale — see `parseSchedulerStore`).
|
|
73
80
|
*/
|
|
74
81
|
export function mutateSchedulerStore(path, fn, opts) {
|
|
75
|
-
const maxRetries = Math.max(0, opts
|
|
82
|
+
const maxRetries = Math.max(0, opts.retries ?? 5);
|
|
76
83
|
const rawOf = () => {
|
|
77
84
|
try {
|
|
78
85
|
return readFileSync(path, 'utf-8');
|
|
@@ -86,16 +93,19 @@ export function mutateSchedulerStore(path, fn, opts) {
|
|
|
86
93
|
};
|
|
87
94
|
for (let attempt = 0;; attempt++) {
|
|
88
95
|
const baseRaw = rawOf();
|
|
89
|
-
const
|
|
90
|
-
const
|
|
96
|
+
const base = baseRaw === null ? { records: [], warnings: [] } : parseSchedulerStore(baseRaw, { nowMs: opts.nowMs });
|
|
97
|
+
const { warnings } = base;
|
|
98
|
+
if (warnings.some((w) => w.kind === 'unreadable-file'))
|
|
99
|
+
return { ok: false, reason: 'unreadable_file', warnings };
|
|
100
|
+
const next = fn(base.records);
|
|
91
101
|
if (next === null)
|
|
92
|
-
return { ok: false, reason: 'aborted' };
|
|
102
|
+
return { ok: false, reason: 'aborted', warnings };
|
|
93
103
|
if (rawOf() !== baseRaw) {
|
|
94
104
|
if (attempt >= maxRetries)
|
|
95
|
-
return { ok: false, reason: 'conflict_exhausted', retries: attempt };
|
|
105
|
+
return { ok: false, reason: 'conflict_exhausted', retries: attempt, warnings };
|
|
96
106
|
continue; // a concurrent writer landed between our read and commit intent — replay on the new base
|
|
97
107
|
}
|
|
98
108
|
saveSchedulerStore(path, next);
|
|
99
|
-
return { ok: true, records: next, retries: attempt };
|
|
109
|
+
return { ok: true, records: next, retries: attempt, warnings };
|
|
100
110
|
}
|
|
101
111
|
}
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* 🔴 `principal` is pinned by the writer from Runner-held ctx (never the model); persisted opaquely, passed
|
|
18
18
|
* through to `runTask` unchanged by the daemon (no privilege escalation).
|
|
19
19
|
*/
|
|
20
|
+
import type { MigrationReceipt } from './migration-receipt.js';
|
|
20
21
|
/** A scheduled `when` — structurally identical to core `ScheduledIntent['when']`, inlined (contract stands alone). */
|
|
21
22
|
export type SchedulerWhen = {
|
|
22
23
|
kind: 'cron';
|
|
@@ -47,31 +48,63 @@ export interface SchedulerRecord {
|
|
|
47
48
|
/**
|
|
48
49
|
* The store ABSTRACTION the two sides depend on (writer saves, reader loads). The `@sema-agent/settings-schema/node`
|
|
49
50
|
* file binding implements it; a server with a DB backend could implement it differently against the same shape.
|
|
51
|
+
* `load()` returns the records AND what the read did to them ({@link SchedulerStoreRead}) — there is no silent read.
|
|
50
52
|
*/
|
|
51
53
|
export interface SchedulerStore {
|
|
52
|
-
load():
|
|
54
|
+
load(): SchedulerStoreRead;
|
|
53
55
|
save(records: SchedulerRecord[]): void;
|
|
54
56
|
}
|
|
55
57
|
/** Structural validation of one record (required fields + a well-formed `when`). Unknown fields are allowed. */
|
|
56
58
|
export declare function isSchedulerRecord(v: unknown): v is SchedulerRecord;
|
|
57
|
-
/**
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
* `
|
|
61
|
-
* - `
|
|
62
|
-
*
|
|
63
|
-
* - `
|
|
64
|
-
*
|
|
59
|
+
/** 一条会话链上的记录过期多久算「永驻孤儿」:`lifetime: "session"` ∧ `sessionId` 在场 ∧ `nextRunMs` 已过去这么久 ⇒ 读口丢弃并告警(7 天)。 */
|
|
60
|
+
export declare const STALE_SESSION_RECORD_MS: number;
|
|
61
|
+
/** {@link parseSchedulerStore} 对它读到的数据**做了什么**(而不是逐字读)。闭集 —— 新 `kind` 是联合加员(穷举 `switch` 的消费方
|
|
62
|
+
* 会编译红,那就是通知)。`index` = 记录在原数组里的下标(对照源文件用)。
|
|
63
|
+
* - `unreadable-file` — 整份文件不是 JSON / 不是数组:读出零条记录。**不是**「空店」:{@link mutateSchedulerStore} 遇它拒写
|
|
64
|
+
* (否则下一次保存会把盘上那份读不懂的数据整份覆盖掉)。
|
|
65
|
+
* - `malformed-record` — 这一条没过 {@link isSchedulerRecord},丢弃;`record` = 原值(未校验,诊断用)。
|
|
66
|
+
* - `stale-session-record` — 会话链上的记录(`lifetime: "session"` ∧ `sessionId` 在场)而 `nextRunMs` 已过去 ≥ {@link STALE_SESSION_RECORD_MS}:
|
|
67
|
+
* 它的会话没走到收割(无常驻 daemon 的车道 / 崩溃),再留着就是永驻。丢弃,不另起写 —— 下一个写者落盘。
|
|
68
|
+
* **只认显式的 `"session"`**:`lifetime` 缺席读作 durable(与 SS-M3 同一只读法);durable 记录带着来源 sessionId
|
|
69
|
+
* 是合法形,daemon 停机一周后的 cron 照样是用户的任务,不许当孤儿丢。
|
|
70
|
+
* - `one-time-migration` — 登记过窗的一次性迁移在读时改写了这一条(今天只有 SS-M3:durable 行按旧会话键存 ⇒ 重铸成
|
|
71
|
+
* `principal ?? "default"`);`receipt` 是迁移的收据(`./one-time-migrations` 的同一形)。 */
|
|
65
72
|
export type SchedulerParseWarning = {
|
|
66
|
-
|
|
73
|
+
kind: 'unreadable-file';
|
|
74
|
+
why: 'not-json' | 'not-an-array';
|
|
75
|
+
} | {
|
|
67
76
|
kind: 'malformed-record';
|
|
77
|
+
index: number;
|
|
68
78
|
record: unknown;
|
|
69
79
|
} | {
|
|
80
|
+
kind: 'stale-session-record';
|
|
81
|
+
index: number;
|
|
82
|
+
id: string;
|
|
83
|
+
sessionId: string;
|
|
84
|
+
nextRunMs: number;
|
|
85
|
+
} | {
|
|
86
|
+
kind: 'one-time-migration';
|
|
70
87
|
index: number;
|
|
71
|
-
|
|
72
|
-
from: string;
|
|
73
|
-
to: string;
|
|
88
|
+
receipt: MigrationReceipt;
|
|
74
89
|
};
|
|
75
|
-
|
|
90
|
+
/** 一次读的结果:留下的记录 + 读时做过的每一件事。没有「静默模式」—— 告警恒随结果返回,调用方决定记到哪。 */
|
|
91
|
+
export interface SchedulerStoreRead {
|
|
92
|
+
readonly records: SchedulerRecord[];
|
|
93
|
+
readonly warnings: SchedulerParseWarning[];
|
|
94
|
+
}
|
|
95
|
+
/** 读口的入参:`nowMs` 由调用方给(本包不读墙钟),判 {@link STALE_SESSION_RECORD_MS}。 */
|
|
96
|
+
export interface SchedulerReadOptions {
|
|
97
|
+
readonly nowMs: number;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Parse the durable store from its serialized form. FORWARD-COMPATIBLE: well-formed records keep ALL their fields (incl.
|
|
101
|
+
* unknown future ones) so a newer writer's data survives an older reader's round-trip. Every deviation from a verbatim
|
|
102
|
+
* read comes back in `warnings` ({@link SchedulerParseWarning}) — 11.0.0 removed the silent mode (the warning callback
|
|
103
|
+
* used to be optional, and the default read paths never passed it, so corrupt records vanished without a trace).
|
|
104
|
+
*
|
|
105
|
+
* 读时改写只有一种:登记在一次性迁移表里、窗内存活的 SS-M3(`migrateSchedulerRecords`,lazy-on-read —— 不另起写,由下一个写者
|
|
106
|
+
* 落盘;另起一次写只会多一个竞争写者)。窗关后随迁移族一起删。
|
|
107
|
+
*/
|
|
108
|
+
export declare function parseSchedulerStore(text: string, opts: SchedulerReadOptions): SchedulerStoreRead;
|
|
76
109
|
/** Serialize the store to its canonical on-disk form (stable, pretty JSON). The WRITE half of the format contract. */
|
|
77
110
|
export declare function serializeSchedulerStore(records: SchedulerRecord[]): string;
|
package/dist/scheduler-store.js
CHANGED
|
@@ -1,22 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
* (R7 自唤醒). The shared seam between the WRITER (server-side `SchedulerCapability`, persists the model's
|
|
4
|
-
* CronCreate/Sleep intents) and the READER (TOC shell `SchedulerDaemon`, fires due intents). One canonical
|
|
5
|
-
* on-disk SHAPE + parse/serialize ⇒ the two independently-built sides never drift on the format.
|
|
6
|
-
*
|
|
7
|
-
* 🔴 Contract discipline (this is a third-party contract lib, not an impl dump):
|
|
8
|
-
* - ABSTRACT: this module is the type + the parse/serialize FORMAT + the `SchedulerStore` interface. It has
|
|
9
|
-
* ZERO runtime IO and ZERO deps (no `@sema-agent/*`, no `node:*`) so it lives in the browser-safe barrel.
|
|
10
|
-
* The concrete filesystem binding is `@sema-agent/settings-schema/node` (`loadSchedulerStore`/`saveSchedulerStore`).
|
|
11
|
-
* - EXTENSIBLE: `parseSchedulerStore` is FORWARD-COMPATIBLE — it validates only the required shape and
|
|
12
|
-
* PRESERVES unknown fields, so a newer writer can add fields without an older reader dropping them on
|
|
13
|
-
* round-trip. New optional `SchedulerRecord` fields are additive (no breaking change).
|
|
14
|
-
* - CONTRACTUAL: `when` is an inlined structural union kept byte-compatible with core `ScheduledIntent['when']`
|
|
15
|
-
* but NOT imported — the contract stands alone, independent of the engine.
|
|
16
|
-
*
|
|
17
|
-
* 🔴 `principal` is pinned by the writer from Runner-held ctx (never the model); persisted opaquely, passed
|
|
18
|
-
* through to `runTask` unchanged by the daemon (no privilege escalation).
|
|
19
|
-
*/
|
|
1
|
+
import { isSessionLifetime } from './scheduler-lifetime.js';
|
|
2
|
+
import { migrateSchedulerRecords } from './scheduler-store-migration.js';
|
|
20
3
|
/** Structural validation of one record (required fields + a well-formed `when`). Unknown fields are allowed. */
|
|
21
4
|
export function isSchedulerRecord(v) {
|
|
22
5
|
if (typeof v !== 'object' || v === null)
|
|
@@ -35,63 +18,51 @@ export function isSchedulerRecord(v) {
|
|
|
35
18
|
return typeof w.delaySec === 'number';
|
|
36
19
|
return false;
|
|
37
20
|
}
|
|
21
|
+
/** 一条会话链上的记录过期多久算「永驻孤儿」:`lifetime: "session"` ∧ `sessionId` 在场 ∧ `nextRunMs` 已过去这么久 ⇒ 读口丢弃并告警(7 天)。 */
|
|
22
|
+
export const STALE_SESSION_RECORD_MS = 7 * 24 * 60 * 60 * 1000;
|
|
38
23
|
/**
|
|
39
|
-
* Parse the durable store from its serialized form.
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
* per dropped record so a caller can surface the loss instead of it vanishing silently (C5/C6 — a dropped
|
|
44
|
-
* record must leave a trace reachable by whoever can act on it).
|
|
45
|
-
*/
|
|
46
|
-
/**
|
|
47
|
-
* core 5.5.0 durable 键分叉的迁移半场([ref];server 裁 (a) 一次性迁移,拒读侧
|
|
48
|
-
* 双键容忍窗)。旧铸键链 `sessionId ?? principal ?? taskId ?? "default"` 让 durable 行的隔离键随会话
|
|
49
|
-
* 走——新会话新 sessionId ⇒ 旧 durable job 永远 not_found。core 5.5.0 起 durable 铸键=
|
|
50
|
-
* `principal ?? "default"`;存量旧键行由**本层**在 parse 时重铸(纯函数,幂等:重写后判据自不满足)。
|
|
24
|
+
* Parse the durable store from its serialized form. FORWARD-COMPATIBLE: well-formed records keep ALL their fields (incl.
|
|
25
|
+
* unknown future ones) so a newer writer's data survives an older reader's round-trip. Every deviation from a verbatim
|
|
26
|
+
* read comes back in `warnings` ({@link SchedulerParseWarning}) — 11.0.0 removed the silent mode (the warning callback
|
|
27
|
+
* used to be optional, and the default read paths never passed it, so corrupt records vanished without a trace).
|
|
51
28
|
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
* 单点同愈,无第二机制。盘面在下一次 save 收敛(读侧恒一致,不做读时写放大)。
|
|
55
|
-
*
|
|
56
|
-
* 窄读判据:`lifetime` 不在本契约的显式键上(server 写侧作为 forward-compat 字段随行),按未知
|
|
57
|
-
* 字段窄读——`"session"` 行是会话链键,零触碰;其余(缺席=durable 默认)按 durable 归一。
|
|
58
|
-
*
|
|
59
|
-
* 落日(2.0.0):删除它 = **持久数据**面的破坏(仍按旧键存着的 durable 行会就此找不回),所以不能在
|
|
60
|
-
* minor 上一删了事 —— 归下一个 MAJOR,按持久数据的规矩办(响亮拒收 + 一次性迁移)。在那之前,本版把
|
|
61
|
-
* 这次重写从**静默**改成**出声**(`durable-scope-rekeyed`,带改写前后):关窗的前提是有人能证明它
|
|
62
|
-
* 已经不再触发,而不是有人记得它还在。
|
|
29
|
+
* 读时改写只有一种:登记在一次性迁移表里、窗内存活的 SS-M3(`migrateSchedulerRecords`,lazy-on-read —— 不另起写,由下一个写者
|
|
30
|
+
* 落盘;另起一次写只会多一个竞争写者)。窗关后随迁移族一起删。
|
|
63
31
|
*/
|
|
64
|
-
function
|
|
65
|
-
if (r.lifetime === 'session')
|
|
66
|
-
return r;
|
|
67
|
-
const want = r.principal ?? 'default';
|
|
68
|
-
return r.scope === want ? r : { ...r, scope: want };
|
|
69
|
-
}
|
|
70
|
-
export function parseSchedulerStore(text, onWarning) {
|
|
32
|
+
export function parseSchedulerStore(text, opts) {
|
|
71
33
|
let parsed;
|
|
72
34
|
try {
|
|
73
35
|
parsed = JSON.parse(text);
|
|
74
36
|
}
|
|
75
37
|
catch {
|
|
76
|
-
return [];
|
|
38
|
+
return { records: [], warnings: [{ kind: 'unreadable-file', why: 'not-json' }] };
|
|
77
39
|
}
|
|
78
40
|
if (!Array.isArray(parsed))
|
|
79
|
-
return [];
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const
|
|
41
|
+
return { records: [], warnings: [{ kind: 'unreadable-file', why: 'not-an-array' }] };
|
|
42
|
+
const warnings = [];
|
|
43
|
+
const kept = [];
|
|
44
|
+
const keptFrom = [];
|
|
83
45
|
parsed.forEach((record, index) => {
|
|
84
46
|
if (!isSchedulerRecord(record)) {
|
|
85
|
-
|
|
47
|
+
warnings.push({ kind: 'malformed-record', index, record });
|
|
86
48
|
return;
|
|
87
49
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
50
|
+
if (isSessionLifetime(record) && typeof record.sessionId === 'string' && record.sessionId !== '' && typeof record.nextRunMs === 'number' && opts.nowMs - record.nextRunMs >= STALE_SESSION_RECORD_MS) {
|
|
51
|
+
warnings.push({ kind: 'stale-session-record', index, id: record.id, sessionId: record.sessionId, nextRunMs: record.nextRunMs });
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
kept.push(record);
|
|
55
|
+
keptFrom.push(index);
|
|
93
56
|
});
|
|
94
|
-
|
|
57
|
+
const migrated = migrateSchedulerRecords(kept);
|
|
58
|
+
for (const receipt of migrated.receipts) {
|
|
59
|
+
// 收据的坐标是迁移入参(`kept`)里的下标;换回原数组下标(对照源文件),与其余告警同一套坐标。
|
|
60
|
+
const at = receipt.path[0];
|
|
61
|
+
const index = typeof at === 'number' ? (keptFrom[at] ?? at) : -1;
|
|
62
|
+
warnings.push({ kind: 'one-time-migration', index, receipt: { ...receipt, path: [index, ...receipt.path.slice(1)] } });
|
|
63
|
+
}
|
|
64
|
+
warnings.sort((a, b) => ('index' in a ? a.index : -1) - ('index' in b ? b.index : -1));
|
|
65
|
+
return { records: migrated.records, warnings };
|
|
95
66
|
}
|
|
96
67
|
/** Serialize the store to its canonical on-disk form (stable, pretty JSON). The WRITE half of the format contract. */
|
|
97
68
|
export function serializeSchedulerStore(records) {
|
package/dist/secret-refs.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Secret-reference inventory (Phase-3 "密钥引用校验").
|
|
2
|
+
* Secret-reference inventory (Phase-3 "密钥引用校验"). web-admin stores only env-NAMEs, never secret
|
|
3
3
|
* values (the ENV_NAME boundary). This collects EVERY env-NAME the effective config references, grouped by
|
|
4
4
|
* name with where each is used — so an operator can audit "which env vars must each host/service provide?"
|
|
5
5
|
* before a worker/model fails at runtime.
|
|
6
6
|
*
|
|
7
|
-
* Note:
|
|
7
|
+
* Note: web-admin can't verify a name actually EXISTS in a consumer's environment (that's the consumer's
|
|
8
8
|
* own env). For workers, the reconciler already reports a missing one as a `SecretMissing` status condition
|
|
9
|
-
* (surfaced on /fleet). This inventory is
|
|
9
|
+
* (surfaced on /fleet). This inventory is web-admin-side half — every referenced env-NAME, incl. the general
|
|
10
10
|
* `secretEnv` passthrough, so a rotation runbook built on it isn't blind to app-specific tokens.
|
|
11
11
|
*/
|
|
12
12
|
import type { EffectiveConfig } from "./types.js";
|
package/dist/secret-refs.js
CHANGED
|
@@ -22,7 +22,7 @@ export function collectEnvRefs(eff) {
|
|
|
22
22
|
for (const [key, envName] of Object.entries(w.runtime.execution?.secretRefs ?? {}))
|
|
23
23
|
add(envName, "workers", `worker "${w.name}".runtime.execution.secretRefs.${key}`);
|
|
24
24
|
// the general secretEnv passthrough also references env-NAMEs (worker-env.ts merges them) — include them
|
|
25
|
-
// so the rotation inventory isn't blind to app-specific tokens like
|
|
25
|
+
// so the rotation inventory isn't blind to app-specific tokens like MY_APP_TOKEN→MY_APP_TOKEN_ENV.
|
|
26
26
|
for (const [key, envName] of Object.entries(w.secretEnv ?? {}))
|
|
27
27
|
add(envName, "workers", `worker "${w.name}".secretEnv.${key}`);
|
|
28
28
|
}
|
package/dist/secret-scan.d.ts
CHANGED
|
@@ -1,28 +1,11 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 秘密扫描读的**归一化**,外加「URL 的 userinfo 就是凭据」那一只判定({@link carriesUserinfo})—— 包内私有模块(6.3.0):不进主 barrel、
|
|
3
|
-
* 不进 `package.json` 的 `exports`,消费方拿不到。
|
|
4
|
-
* 校验侧探测器 `containsSecretToken`(`./types`)只读 {@link percentLayers};展示侧脱敏器 `redactSecrets`(`./config-fns`)
|
|
5
|
-
* 读 {@link secretScanForms}。展示侧的读法(边界读法、`+` 读法、「没走完」那一位)刻意不做公开契约:它们以后要随新发现的
|
|
6
|
-
* 编码形调整,调整不该是契约变更(opus 独立腿 L7)。
|
|
7
|
-
*
|
|
8
|
-
* **单遍、零异常**(opus 独立腿 M1,对 6.2.0 的读路径 CPU 回归):6.2.0 起的宽容解码对每个 `%HH` 各 `try decodeURIComponent`,
|
|
9
|
-
* 非 ASCII 的转义每个都抛一次异常(≈ 20–30 µs / 字节,同步阻塞);6.3.0 首稿又让探测器顺带算它不用的展示形。这里两处解码都
|
|
10
|
-
* 不靠异常:单个转义的判据写成字符集(只有 ASCII 的 `%00`–`%7F` 单独解得开),转义串的判据写成 UTF-8 良构判定 ——
|
|
11
|
-
* 结果与原来的 try/catch 逐字相同(`test/disclosable-identity-6.3.0.test.ts` 的两只冻结 oracle 钉着)。
|
|
12
|
-
*
|
|
13
|
-
* 本模块零 import(`./types` 引它,它不回引,不成环)。
|
|
14
|
-
*/
|
|
15
1
|
/** How many percent-decoded layers a secret scan reads beyond the raw string (a multi-encoded `%2573%256B…` →
|
|
16
2
|
* `%73%6B…` → `sk-…`). Bounded → deterministic. */
|
|
17
3
|
export declare const SECRET_SCAN_DECODE_LAYERS = 3;
|
|
18
|
-
/** The raw string first, then up to {@link SECRET_SCAN_DECODE_LAYERS} tolerant percent-decoded layers, stopping at the first
|
|
19
|
-
* layer that decodes nothing — the forms the validation detector `containsSecretToken` reads, and nothing else. */
|
|
20
|
-
export declare function percentLayers(text: string): readonly string[];
|
|
21
4
|
/** What {@link secretScanForms} returns — plain data, no behavior. */
|
|
22
5
|
export interface SecretScanForms {
|
|
23
6
|
/** {@link percentLayers} of the text. */
|
|
24
7
|
readonly percentLayers: readonly string[];
|
|
25
|
-
/** The forms
|
|
8
|
+
/** The forms a string is judged on (write and display alike): `percentLayers`, each layer's {@link boundaryReading}, and
|
|
26
9
|
* each of those read the application/x-www-form-urlencoded way (`+` as a space); duplicates dropped, first seen first. */
|
|
27
10
|
readonly forms: readonly string[];
|
|
28
11
|
/** Normalization did not finish: the layer budget ran out while one more layer would still decode something. A caller that
|
|
@@ -30,10 +13,17 @@ export interface SecretScanForms {
|
|
|
30
13
|
* point, not an unfinished one: `%E4%B8%AD` stays as it is and is not flagged.) */
|
|
31
14
|
readonly unresolved: boolean;
|
|
32
15
|
}
|
|
33
|
-
/** The
|
|
16
|
+
/** The normalization BOTH scanners read (11.0.0): the validation detector `containsSecretToken` (types) and the display redactor
|
|
17
|
+
* `redactSecrets` (config-fns). */
|
|
34
18
|
export declare function secretScanForms(text: string): SecretScanForms;
|
|
35
|
-
/** 「URL 的 userinfo 就是凭据」的**唯一**判定(6.3.0
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
|
|
19
|
+
/** 「URL 的 userinfo 就是凭据」的**唯一**判定(6.3.0 起;11.0.0 S-811 扩到自由文本):文本是带 userinfo 的 URL(URL 形判官
|
|
20
|
+
* {@link httpUrlFlaws} 判出 `userinfo` 这一件 —— 层级式任意 scheme,`ssh://git@host` 只带用户名也算),或文本里某个 `@` 收尾的那一段
|
|
21
|
+
* (从 `@` 向前到空白或上一个 `@`)是凭据对:
|
|
22
|
+
* · authority 形 —— 段里最后一个 `//` 到 `@` 之间非空且不含 `/ ? #`(`jdbc:postgresql://svc:pw@…`、`//admin:pw@…`、`ssh://git@h`);
|
|
23
|
+
* · 冒号形 —— 段里最后一个 `/ ? #` 之后的那一截是 `user:pw`(两侧非空),且 `@` 后紧跟主机字符(`admin:pw@db:5432/app`、`postgres:/svc:pw@h`)。
|
|
24
|
+
* 引号 / 尖括号 / 反引号**不**断段(它们可以在口令里)。单遍:每个字符至多被一次向前扫描读到(段以上一个 `@` 为界、互不重叠)⇒ 线性,
|
|
25
|
+
* 不用回溯正则(codex r1:扫描正则在 `;` × 4 万 + `@` 上要 10 s)。
|
|
26
|
+
* 读者:展示侧 `redactSecrets`(`./config-fns`)对空白分隔的候选段与归一化形各问一次 —— 自由文本出门面(坐标 / 错误句 / 主机名标签)与身份面
|
|
27
|
+
* (`isDisclosableIdentity` 问的就是 `redactSecrets` 原样放不放行)同一条律。校验侧不读它:URL 座位的 userinfo 由 URL 形判官拒,自由文本写口
|
|
28
|
+
* 不因 userinfo 拒(契约未变)。负控:路径里的 `@`(`https://h/@scope/pkg`)、邮箱、`name@版本`、空 userinfo(`https://@h`)都不是。 */
|
|
29
|
+
export declare function carriesUserinfo(text: string): boolean;
|