@sema-agent/settings-schema 1.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.
Files changed (56) hide show
  1. package/CHANGELOG.md +728 -0
  2. package/LICENSE +103 -0
  3. package/README.md +129 -0
  4. package/dist/api/auth-bridge.d.ts +331 -0
  5. package/dist/api/auth-bridge.js +210 -0
  6. package/dist/api/auth.d.ts +216 -0
  7. package/dist/api/auth.js +138 -0
  8. package/dist/api/scopes.d.ts +344 -0
  9. package/dist/api/scopes.js +222 -0
  10. package/dist/api/wire.d.ts +60 -0
  11. package/dist/api/wire.js +89 -0
  12. package/dist/bundle.d.ts +13 -0
  13. package/dist/bundle.js +67 -0
  14. package/dist/config-fns.d.ts +318 -0
  15. package/dist/config-fns.js +472 -0
  16. package/dist/cross-domain.d.ts +34 -0
  17. package/dist/cross-domain.js +118 -0
  18. package/dist/file-edit.d.ts +36 -0
  19. package/dist/file-edit.js +125 -0
  20. package/dist/file-store.d.ts +89 -0
  21. package/dist/file-store.js +238 -0
  22. package/dist/fleet.d.ts +498 -0
  23. package/dist/fleet.js +317 -0
  24. package/dist/hash.d.ts +32 -0
  25. package/dist/hash.js +59 -0
  26. package/dist/hooks.d.ts +5477 -0
  27. package/dist/hooks.js +627 -0
  28. package/dist/index.d.ts +22 -0
  29. package/dist/index.js +26 -0
  30. package/dist/local-load.d.ts +42 -0
  31. package/dist/local-load.js +172 -0
  32. package/dist/migrate.d.ts +255 -0
  33. package/dist/migrate.js +542 -0
  34. package/dist/node.d.ts +11 -0
  35. package/dist/node.js +11 -0
  36. package/dist/reader.d.ts +18 -0
  37. package/dist/reader.js +1 -0
  38. package/dist/remote-exec.d.ts +274 -0
  39. package/dist/remote-exec.js +182 -0
  40. package/dist/resolve-roster.d.ts +28 -0
  41. package/dist/resolve-roster.js +108 -0
  42. package/dist/safety-merge-spec.d.ts +327 -0
  43. package/dist/safety-merge-spec.js +70 -0
  44. package/dist/scheduler-store-node.d.ts +72 -0
  45. package/dist/scheduler-store-node.js +119 -0
  46. package/dist/scheduler-store.d.ts +67 -0
  47. package/dist/scheduler-store.js +89 -0
  48. package/dist/secret-refs.d.ts +33 -0
  49. package/dist/secret-refs.js +48 -0
  50. package/dist/sha256.d.ts +16 -0
  51. package/dist/sha256.js +114 -0
  52. package/dist/skills-manifest.d.ts +12 -0
  53. package/dist/skills-manifest.js +54 -0
  54. package/dist/types.d.ts +13560 -0
  55. package/dist/types.js +2118 -0
  56. package/package.json +138 -0
@@ -0,0 +1,327 @@
1
+ /**
2
+ * #14 SAFETY MERGE-SPEC — the ONE machine-readable per-category merge/enforcement table that every settings
3
+ * resolver (TOC shell-adapter + TOB center/service) reads, so they never re-implement (and drift on) the merge
4
+ * law. This formalizes settings-core's v2-design §3 merge-spec into a published-data contract; the SEMANTIC
5
+ * author is settings-core, center maintains the formalized artifact here (registry-core), the axis VALUE domains
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, center's
8
+ * config-field→category map); THIS file is only the shared category spec they all cite — no third copy of the law.
9
+ *
10
+ * Per-category behavior is fully determined by `mergeShape` + `minTrust` + `tighten` + `enforcement`; a resolver
11
+ * reads this table and has NO per-key special-casing.
12
+ */
13
+ import { z } from "zod";
14
+ /** How values from different layers/principals combine for a category. */
15
+ export declare const MergeShape: z.ZodEnum<["denyFirst", "replace", "runAll", "overrideByName", "concat"]>;
16
+ export type MergeShape = z.infer<typeof MergeShape>;
17
+ /** The MINIMUM trust tier a layer/principal must have to set this category (ascending trust). */
18
+ export declare const MinTrust: z.ZodEnum<["local", "session", "project", "global", "managed"]>;
19
+ export type MinTrust = z.infer<typeof MinTrust>;
20
+ /** WHERE the category is enforced (STRENGTH) — MUST stay in lockstep with `EffectiveKey.enforcement` (types.ts).
21
+ * gate=data-plane engine, server=control-plane/TOB boundary, resolver=merger correctness, client=UI only. */
22
+ export declare const SpecEnforcement: z.ZodEnum<["gate", "server", "resolver", "client"]>;
23
+ export type SpecEnforcement = z.infer<typeof SpecEnforcement>;
24
+ /** Fail direction when enforcement can't be evaluated. fail-OPEN is allowed for `client` OR `resolver`
25
+ * enforcement on NON-secret keys (additive merges like mcpServers/rules/env); `gate`/`server`/secret → CLOSED. */
26
+ export declare const FailMode: z.ZodEnum<["closed", "open"]>;
27
+ export type FailMode = z.infer<typeof FailMode>;
28
+ /** The monotone tighten-only direction for a category (which way is "stricter"). `enumRank.order` lists values
29
+ * from LOOSEST→STRICTEST; `boolStricter.strict` is the bool value that is the tighter one; `setNarrower` = a
30
+ * subset is tighter; `policyDenyWins` = deny beats allow; `none` = not rank-tightenable (replace / conflict-reject). */
31
+ export declare const TightenRule: z.ZodDiscriminatedUnion<"rule", [z.ZodObject<{
32
+ rule: z.ZodLiteral<"none">;
33
+ }, "strip", z.ZodTypeAny, {
34
+ rule: "none";
35
+ }, {
36
+ rule: "none";
37
+ }>, z.ZodObject<{
38
+ rule: z.ZodLiteral<"policyDenyWins">;
39
+ }, "strip", z.ZodTypeAny, {
40
+ rule: "policyDenyWins";
41
+ }, {
42
+ rule: "policyDenyWins";
43
+ }>, z.ZodObject<{
44
+ rule: z.ZodLiteral<"setNarrower">;
45
+ }, "strip", z.ZodTypeAny, {
46
+ rule: "setNarrower";
47
+ }, {
48
+ rule: "setNarrower";
49
+ }>, z.ZodObject<{
50
+ rule: z.ZodLiteral<"boolStricter">;
51
+ strict: z.ZodBoolean;
52
+ }, "strip", z.ZodTypeAny, {
53
+ strict: boolean;
54
+ rule: "boolStricter";
55
+ }, {
56
+ strict: boolean;
57
+ rule: "boolStricter";
58
+ }>, z.ZodObject<{
59
+ rule: z.ZodLiteral<"enumRank">;
60
+ order: z.ZodArray<z.ZodString, "many">;
61
+ }, "strip", z.ZodTypeAny, {
62
+ rule: "enumRank";
63
+ order: string[];
64
+ }, {
65
+ rule: "enumRank";
66
+ order: string[];
67
+ }>]>;
68
+ export type TightenRule = z.infer<typeof TightenRule>;
69
+ export declare const MergeCategorySpec: z.ZodObject<{
70
+ mergeShape: z.ZodEnum<["denyFirst", "replace", "runAll", "overrideByName", "concat"]>;
71
+ minTrust: z.ZodEnum<["local", "session", "project", "global", "managed"]>;
72
+ tighten: z.ZodDiscriminatedUnion<"rule", [z.ZodObject<{
73
+ rule: z.ZodLiteral<"none">;
74
+ }, "strip", z.ZodTypeAny, {
75
+ rule: "none";
76
+ }, {
77
+ rule: "none";
78
+ }>, z.ZodObject<{
79
+ rule: z.ZodLiteral<"policyDenyWins">;
80
+ }, "strip", z.ZodTypeAny, {
81
+ rule: "policyDenyWins";
82
+ }, {
83
+ rule: "policyDenyWins";
84
+ }>, z.ZodObject<{
85
+ rule: z.ZodLiteral<"setNarrower">;
86
+ }, "strip", z.ZodTypeAny, {
87
+ rule: "setNarrower";
88
+ }, {
89
+ rule: "setNarrower";
90
+ }>, z.ZodObject<{
91
+ rule: z.ZodLiteral<"boolStricter">;
92
+ strict: z.ZodBoolean;
93
+ }, "strip", z.ZodTypeAny, {
94
+ strict: boolean;
95
+ rule: "boolStricter";
96
+ }, {
97
+ strict: boolean;
98
+ rule: "boolStricter";
99
+ }>, z.ZodObject<{
100
+ rule: z.ZodLiteral<"enumRank">;
101
+ order: z.ZodArray<z.ZodString, "many">;
102
+ }, "strip", z.ZodTypeAny, {
103
+ rule: "enumRank";
104
+ order: string[];
105
+ }, {
106
+ rule: "enumRank";
107
+ order: string[];
108
+ }>]>;
109
+ enforcement: z.ZodEnum<["gate", "server", "resolver", "client"]>;
110
+ fail: z.ZodEnum<["closed", "open"]>;
111
+ /** The compile target / seam this category lands on (free text, for traceability). */
112
+ seam: z.ZodString;
113
+ /** When the category's VALUE domain is core's, the cited `SAFETY_AXIS_VOCABULARY` dimension (conformance-checked). */
114
+ coreAxis: z.ZodOptional<z.ZodEnum<["toolEffect", "egress", "irreversibility", "safetyAxis", "severity", "shellGate", "permissionDecision"]>>;
115
+ }, "strip", z.ZodTypeAny, {
116
+ enforcement: "gate" | "server" | "resolver" | "client";
117
+ mergeShape: "concat" | "replace" | "denyFirst" | "runAll" | "overrideByName";
118
+ minTrust: "local" | "session" | "global" | "project" | "managed";
119
+ tighten: {
120
+ rule: "none";
121
+ } | {
122
+ rule: "policyDenyWins";
123
+ } | {
124
+ rule: "setNarrower";
125
+ } | {
126
+ strict: boolean;
127
+ rule: "boolStricter";
128
+ } | {
129
+ rule: "enumRank";
130
+ order: string[];
131
+ };
132
+ fail: "closed" | "open";
133
+ seam: string;
134
+ coreAxis?: "permissionDecision" | "toolEffect" | "egress" | "irreversibility" | "safetyAxis" | "severity" | "shellGate" | undefined;
135
+ }, {
136
+ enforcement: "gate" | "server" | "resolver" | "client";
137
+ mergeShape: "concat" | "replace" | "denyFirst" | "runAll" | "overrideByName";
138
+ minTrust: "local" | "session" | "global" | "project" | "managed";
139
+ tighten: {
140
+ rule: "none";
141
+ } | {
142
+ rule: "policyDenyWins";
143
+ } | {
144
+ rule: "setNarrower";
145
+ } | {
146
+ strict: boolean;
147
+ rule: "boolStricter";
148
+ } | {
149
+ rule: "enumRank";
150
+ order: string[];
151
+ };
152
+ fail: "closed" | "open";
153
+ seam: string;
154
+ coreAxis?: "permissionDecision" | "toolEffect" | "egress" | "irreversibility" | "safetyAxis" | "severity" | "shellGate" | undefined;
155
+ }>;
156
+ export type MergeCategorySpec = z.infer<typeof MergeCategorySpec>;
157
+ /** The category spec. Keys = the unified settings categories both seats map their concrete keys/fields into.
158
+ * Faithful to settings-core v2-design §3 (the semantic source of record). */
159
+ export declare const SAFETY_MERGE_SPEC: {
160
+ readonly permissions: {
161
+ readonly mergeShape: "denyFirst";
162
+ readonly minTrust: "global";
163
+ readonly tighten: {
164
+ readonly rule: "policyDenyWins";
165
+ };
166
+ readonly enforcement: "gate";
167
+ readonly fail: "closed";
168
+ readonly seam: "toolPolicy + deny-narrowing (SessionPolicyStore)";
169
+ readonly coreAxis: "permissionDecision";
170
+ };
171
+ readonly permissionMode: {
172
+ readonly mergeShape: "replace";
173
+ readonly minTrust: "global";
174
+ readonly tighten: {
175
+ readonly rule: "enumRank";
176
+ readonly order: ["bypassPermissions", "auto", "acceptEdits", "default", "dontAsk", "plan"];
177
+ };
178
+ readonly enforcement: "gate";
179
+ readonly fail: "closed";
180
+ readonly seam: "derive → toolPolicy / onAsk / handsReadOnly (§6.4); bypass = launch-flag only + MANAGED kill-switch";
181
+ };
182
+ readonly handsReadOnly: {
183
+ readonly mergeShape: "replace";
184
+ readonly minTrust: "global";
185
+ readonly tighten: {
186
+ readonly rule: "boolStricter";
187
+ readonly strict: true;
188
+ };
189
+ readonly enforcement: "gate";
190
+ readonly fail: "closed";
191
+ readonly seam: "handsReadOnly";
192
+ };
193
+ readonly shellGate: {
194
+ readonly mergeShape: "replace";
195
+ readonly minTrust: "global";
196
+ readonly tighten: {
197
+ readonly rule: "enumRank";
198
+ readonly order: ["off", "classify", "always"];
199
+ };
200
+ readonly enforcement: "gate";
201
+ readonly fail: "closed";
202
+ readonly seam: "shellGate";
203
+ readonly coreAxis: "shellGate";
204
+ };
205
+ readonly approver: {
206
+ readonly mergeShape: "replace";
207
+ readonly minTrust: "global";
208
+ readonly tighten: {
209
+ readonly rule: "none";
210
+ };
211
+ readonly enforcement: "gate";
212
+ readonly fail: "closed";
213
+ readonly seam: "onAsk (conflict → reject)";
214
+ };
215
+ readonly sandboxNetwork: {
216
+ readonly mergeShape: "denyFirst";
217
+ readonly minTrust: "global";
218
+ readonly tighten: {
219
+ readonly rule: "setNarrower";
220
+ };
221
+ readonly enforcement: "gate";
222
+ readonly fail: "closed";
223
+ readonly seam: "ExecutionEnv (fs+net) + sensitive-path";
224
+ readonly coreAxis: "egress";
225
+ };
226
+ readonly hooks: {
227
+ readonly mergeShape: "runAll";
228
+ readonly minTrust: "project";
229
+ readonly tighten: {
230
+ readonly rule: "none";
231
+ };
232
+ readonly enforcement: "gate";
233
+ readonly fail: "closed";
234
+ readonly seam: "hooks (single source or adapter-composed)";
235
+ };
236
+ readonly mcpServers: {
237
+ readonly mergeShape: "overrideByName";
238
+ readonly minTrust: "project";
239
+ readonly tighten: {
240
+ readonly rule: "none";
241
+ };
242
+ readonly enforcement: "resolver";
243
+ readonly fail: "open";
244
+ readonly seam: "spec.mcp + allowTools";
245
+ };
246
+ readonly rulesInstructions: {
247
+ readonly mergeShape: "concat";
248
+ readonly minTrust: "project";
249
+ readonly tighten: {
250
+ readonly rule: "none";
251
+ };
252
+ readonly enforcement: "resolver";
253
+ readonly fail: "open";
254
+ readonly seam: "appendSystemPrompt / memory (GLOBAL→PROJECT→SESSION)";
255
+ };
256
+ readonly model: {
257
+ readonly mergeShape: "replace";
258
+ readonly minTrust: "session";
259
+ readonly tighten: {
260
+ readonly rule: "none";
261
+ };
262
+ readonly enforcement: "client";
263
+ readonly fail: "open";
264
+ readonly seam: "RoleSpec / TaskSpec.{model,thinking}";
265
+ };
266
+ readonly env: {
267
+ readonly mergeShape: "concat";
268
+ readonly minTrust: "project";
269
+ readonly tighten: {
270
+ readonly rule: "none";
271
+ };
272
+ readonly enforcement: "resolver";
273
+ readonly fail: "open";
274
+ readonly seam: "NodeExecutionEnv.shellEnv (later layer overrides same name)";
275
+ };
276
+ readonly envSecret: {
277
+ readonly mergeShape: "replace";
278
+ readonly minTrust: "global";
279
+ readonly tighten: {
280
+ readonly rule: "none";
281
+ };
282
+ readonly enforcement: "resolver";
283
+ readonly fail: "closed";
284
+ readonly seam: "§7 secret-ref resolution";
285
+ };
286
+ readonly memoryScopes: {
287
+ readonly mergeShape: "concat";
288
+ readonly minTrust: "global";
289
+ readonly tighten: {
290
+ readonly rule: "none";
291
+ };
292
+ readonly enforcement: "client";
293
+ readonly fail: "open";
294
+ readonly seam: "MemoryStore";
295
+ };
296
+ readonly autoCompaction: {
297
+ readonly mergeShape: "replace";
298
+ readonly minTrust: "session";
299
+ readonly tighten: {
300
+ readonly rule: "none";
301
+ };
302
+ readonly enforcement: "client";
303
+ readonly fail: "open";
304
+ readonly seam: "compaction config";
305
+ };
306
+ readonly ui: {
307
+ readonly mergeShape: "replace";
308
+ readonly minTrust: "local";
309
+ readonly tighten: {
310
+ readonly rule: "none";
311
+ };
312
+ readonly enforcement: "client";
313
+ readonly fail: "open";
314
+ readonly seam: "shell-only (theme/keybindings/statusLine) — never touches the engine";
315
+ };
316
+ readonly org: {
317
+ readonly mergeShape: "replace";
318
+ readonly minTrust: "managed";
319
+ readonly tighten: {
320
+ readonly rule: "none";
321
+ };
322
+ readonly enforcement: "server";
323
+ readonly fail: "closed";
324
+ readonly seam: "config-center → governanceBaseline (SSO/SCIM/audit/budget, TOB-only)";
325
+ };
326
+ };
327
+ export type SafetyMergeCategory = keyof typeof SAFETY_MERGE_SPEC;
@@ -0,0 +1,70 @@
1
+ /**
2
+ * #14 SAFETY MERGE-SPEC — the ONE machine-readable per-category merge/enforcement table that every settings
3
+ * resolver (TOC shell-adapter + TOB center/service) reads, so they never re-implement (and drift on) the merge
4
+ * law. This formalizes settings-core's v2-design §3 merge-spec into a published-data contract; the SEMANTIC
5
+ * author is settings-core, center maintains the formalized artifact here (registry-core), the axis VALUE domains
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, center's
8
+ * config-field→category map); THIS file is only the shared category spec they all cite — no third copy of the law.
9
+ *
10
+ * Per-category behavior is fully determined by `mergeShape` + `minTrust` + `tighten` + `enforcement`; a resolver
11
+ * reads this table and has NO per-key special-casing.
12
+ */
13
+ import { z } from "zod";
14
+ /** How values from different layers/principals combine for a category. */
15
+ export const MergeShape = z.enum(["denyFirst", "replace", "runAll", "overrideByName", "concat"]);
16
+ /** The MINIMUM trust tier a layer/principal must have to set this category (ascending trust). */
17
+ export const MinTrust = z.enum(["local", "session", "project", "global", "managed"]);
18
+ /** WHERE the category is enforced (STRENGTH) — MUST stay in lockstep with `EffectiveKey.enforcement` (types.ts).
19
+ * gate=data-plane engine, server=control-plane/TOB boundary, resolver=merger correctness, client=UI only. */
20
+ export const SpecEnforcement = z.enum(["gate", "server", "resolver", "client"]);
21
+ /** Fail direction when enforcement can't be evaluated. fail-OPEN is allowed for `client` OR `resolver`
22
+ * enforcement on NON-secret keys (additive merges like mcpServers/rules/env); `gate`/`server`/secret → CLOSED. */
23
+ export const FailMode = z.enum(["closed", "open"]);
24
+ /** The monotone tighten-only direction for a category (which way is "stricter"). `enumRank.order` lists values
25
+ * from LOOSEST→STRICTEST; `boolStricter.strict` is the bool value that is the tighter one; `setNarrower` = a
26
+ * subset is tighter; `policyDenyWins` = deny beats allow; `none` = not rank-tightenable (replace / conflict-reject). */
27
+ export const TightenRule = z.discriminatedUnion("rule", [
28
+ z.object({ rule: z.literal("none") }),
29
+ z.object({ rule: z.literal("policyDenyWins") }),
30
+ z.object({ rule: z.literal("setNarrower") }),
31
+ z.object({ rule: z.literal("boolStricter"), strict: z.boolean() }),
32
+ z.object({ rule: z.literal("enumRank"), order: z.array(z.string()).min(2) }),
33
+ ]);
34
+ export const MergeCategorySpec = z.object({
35
+ mergeShape: MergeShape,
36
+ minTrust: MinTrust,
37
+ tighten: TightenRule,
38
+ enforcement: SpecEnforcement,
39
+ fail: FailMode,
40
+ /** The compile target / seam this category lands on (free text, for traceability). */
41
+ seam: z.string(),
42
+ /** When the category's VALUE domain is core's, the cited `SAFETY_AXIS_VOCABULARY` dimension (conformance-checked). */
43
+ coreAxis: z.enum(["toolEffect", "egress", "irreversibility", "safetyAxis", "severity", "shellGate", "permissionDecision"]).optional(),
44
+ });
45
+ /** The category spec. Keys = the unified settings categories both seats map their concrete keys/fields into.
46
+ * Faithful to settings-core v2-design §3 (the semantic source of record). */
47
+ export const SAFETY_MERGE_SPEC = {
48
+ permissions: { mergeShape: "denyFirst", minTrust: "global", tighten: { rule: "policyDenyWins" }, enforcement: "gate", fail: "closed", seam: "toolPolicy + deny-narrowing (SessionPolicyStore)", coreAxis: "permissionDecision" },
49
+ // [3759] order covers the FULL shell-addressable mode vocabulary — a word missing here is not "unknown", it
50
+ // makes every two-layer merge containing it hit the out-of-vocabulary reject arm (the whole bundle used to be
51
+ // dropped; shells now reject per-unit, but the semantic fix is a complete rank). Rank anchors (server
52
+ // task-settings.ts translation): bypass ⇒ shellGate off; auto ⇒ classify with classifier auto-release over ANY
53
+ // benign ask (⊇ acceptEdits' edits-only auto-allow); dontAsk = every would-ask → deny (allow rules still pass,
54
+ // tighter than default, looser than plan's handsReadOnly).
55
+ 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" },
56
+ handsReadOnly: { mergeShape: "replace", minTrust: "global", tighten: { rule: "boolStricter", strict: true }, enforcement: "gate", fail: "closed", seam: "handsReadOnly" },
57
+ shellGate: { mergeShape: "replace", minTrust: "global", tighten: { rule: "enumRank", order: ["off", "classify", "always"] }, enforcement: "gate", fail: "closed", seam: "shellGate", coreAxis: "shellGate" },
58
+ approver: { mergeShape: "replace", minTrust: "global", tighten: { rule: "none" }, enforcement: "gate", fail: "closed", seam: "onAsk (conflict → reject)" },
59
+ sandboxNetwork: { mergeShape: "denyFirst", minTrust: "global", tighten: { rule: "setNarrower" }, enforcement: "gate", fail: "closed", seam: "ExecutionEnv (fs+net) + sensitive-path", coreAxis: "egress" },
60
+ hooks: { mergeShape: "runAll", minTrust: "project", tighten: { rule: "none" }, enforcement: "gate", fail: "closed", seam: "hooks (single source or adapter-composed)" },
61
+ mcpServers: { mergeShape: "overrideByName", minTrust: "project", tighten: { rule: "none" }, enforcement: "resolver", fail: "open", seam: "spec.mcp + allowTools" },
62
+ rulesInstructions: { mergeShape: "concat", minTrust: "project", tighten: { rule: "none" }, enforcement: "resolver", fail: "open", seam: "appendSystemPrompt / memory (GLOBAL→PROJECT→SESSION)" },
63
+ model: { mergeShape: "replace", minTrust: "session", tighten: { rule: "none" }, enforcement: "client", fail: "open", seam: "RoleSpec / TaskSpec.{model,thinking}" },
64
+ env: { mergeShape: "concat", minTrust: "project", tighten: { rule: "none" }, enforcement: "resolver", fail: "open", seam: "NodeExecutionEnv.shellEnv (later layer overrides same name)" },
65
+ envSecret: { mergeShape: "replace", minTrust: "global", tighten: { rule: "none" }, enforcement: "resolver", fail: "closed", seam: "§7 secret-ref resolution" },
66
+ memoryScopes: { mergeShape: "concat", minTrust: "global", tighten: { rule: "none" }, enforcement: "client", fail: "open", seam: "MemoryStore" },
67
+ autoCompaction: { mergeShape: "replace", minTrust: "session", tighten: { rule: "none" }, enforcement: "client", fail: "open", seam: "compaction config" },
68
+ ui: { mergeShape: "replace", minTrust: "local", tighten: { rule: "none" }, enforcement: "client", fail: "open", seam: "shell-only (theme/keybindings/statusLine) — never touches the engine" },
69
+ org: { mergeShape: "replace", minTrust: "managed", tighten: { rule: "none" }, enforcement: "server", fail: "closed", seam: "config-center → governanceBaseline (SSO/SCIM/audit/budget, TOB-only)" },
70
+ };
@@ -0,0 +1,72 @@
1
+ import { type SchedulerRecord, type SchedulerStore } from './scheduler-store.js';
2
+ /**
3
+ * Load the durable store. A MISSING file (ENOENT) ⇒ `[]` (fail-soft, first-run/never-written case); any OTHER
4
+ * read failure (EACCES/EIO/EISDIR/…) RETHROWS — only absence means "empty", not "unreadable for any reason"
5
+ * (a permissions/IO fault silently treated as [] would let a caller-side save() overwrite real on-disk data with
6
+ * an empty store). The pure parser drops corrupt individual records and preserves well-formed ones
7
+ * (forward-compatible) — that degradation stays scoped to malformed CONTENT, not to read failures.
8
+ */
9
+ export declare function loadSchedulerStore(path: string): SchedulerRecord[];
10
+ /**
11
+ * Persist ATOMICALLY: `mkdir -p` the parent, write a uniquely-named `.tmp` sibling (mode 0o600 — owner-only, the
12
+ * records may carry principal/session identifiers), then `rename` over the target (atomic on POSIX, so a
13
+ * concurrent reader never sees a partial file). Throws on IO failure (caller maps to a domain error).
14
+ */
15
+ export declare function saveSchedulerStore(path: string, records: SchedulerRecord[]): void;
16
+ /** A filesystem-backed {@link SchedulerStore} bound to a path (convenience for the daemon/backend constructors). */
17
+ export declare function fileSchedulerStore(path: string): SchedulerStore;
18
+ /** Outcome of {@link mutateSchedulerStore}. `conflict_exhausted` = a concurrent writer kept changing the file
19
+ * faster than the bounded replays; nothing was written on that final attempt (earlier attempts never wrote). */
20
+ export type SchedulerStoreMutation = {
21
+ ok: true;
22
+ records: SchedulerRecord[];
23
+ retries: number;
24
+ } | {
25
+ ok: false;
26
+ reason: 'conflict_exhausted';
27
+ retries: number;
28
+ } | {
29
+ ok: false;
30
+ reason: 'aborted';
31
+ };
32
+ /**
33
+ * The ONE cross-process mutation primitive for the shared scheduler store (board [1001]①/[1003]② —
34
+ * server L1-2 + shell 对抗评审 F1 双向合流后的契约层统一原语). Every WRITER (engine backend, resident
35
+ * daemon fire/reap, session reap, sweeps) must go through this instead of hand-rolled
36
+ * load→filter→save — the naked sequence is a lost-update race: two writers load the same base
37
+ * array, mutate independently, and the later `save` silently reverts the earlier one.
38
+ *
39
+ * Optimistic concurrency, format-preserving (no envelope/sidecar/lock file):
40
+ * 1. read the RAW file text and keep it as the base witness (missing file ⇒ empty witness);
41
+ * 2. run `fn(records)` — MUST be pure/replayable; return the next records array, or `null` to
42
+ * abort with no write (e.g. "nothing to do" discovered under the latest base);
43
+ * 3. re-read the raw text just before committing — if it no longer equals the witness, a
44
+ * concurrent writer landed: REPLAY from step 1 (bounded by `retries`, default 5);
45
+ * 4. commit via the same tmp+rename atomic write as {@link saveSchedulerStore}.
46
+ *
47
+ * Honest limits (declared in [1003], server ACK'd): the witness-check→rename window is not zero —
48
+ * this SHRINKS the race from "the whole read-modify-write span" to microseconds, it does not
49
+ * eliminate it (that would need an advisory lock protocol every writer joins; rejected for
50
+ * stale-lock ops burden + win32 friction). `fn` re-runs on replay: side effects inside `fn` are a
51
+ * caller bug.
52
+ */
53
+ export declare function mutateSchedulerStore(path: string, fn: (records: SchedulerRecord[]) => SchedulerRecord[] | null, opts?: {
54
+ retries?: number;
55
+ }): SchedulerStoreMutation;
56
+ /** [1057]① prompts contentDigest(node 绑定;browser-safe 纯层不带 crypto):
57
+ * canonical JSON(stableStringify 同姿势:键序稳定+深度护栏)全量 sha256,**非盐化**
58
+ * ([1050]② operator 对账位——center 产、server 透传、core manifest 归因三方可独立复算)。
59
+ * 入参=下发形的 {sections, scenarioOverrides?}(packId/contentDigest 自身不入 hash)。 */
60
+ export declare function promptsContentDigest(payload: {
61
+ sections: unknown;
62
+ scenarioOverrides?: unknown;
63
+ }): {
64
+ contentDigest: string;
65
+ packId: string;
66
+ /** [1062]③ 顺带:逐 section 对 `text` 铸的 contentHash(与 core 1.315 校验语义同刻度=
67
+ * sha256(text));元素无 string text 位=undefined。center 物化流程=先把这些写回
68
+ * declaration.contentHash,再对含 hash 的终形铸整包 digest(本函数对入参原样铸,两步自洽)。 */
69
+ sectionHashes: Array<string | undefined>;
70
+ };
71
+ /** 逐 section 对账锚(core 1.315 `PromptTextDeclaration.contentHash` 同刻度):sha256(text)。 */
72
+ export declare function sectionContentHash(text: string): string;
@@ -0,0 +1,119 @@
1
+ /**
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 registry-core's browser-safe discipline. Both
4
+ * the server-side writer and the TOC shell daemon import THIS for the on-disk store; the FORMAT lives in the
5
+ * pure `./scheduler-store` contract, so there is one shape and no drift.
6
+ *
7
+ * Store path = caller-supplied; both sides agree on `SEMA_CONFIG_DIR` → `~/.sema/scheduled_tasks.json`.
8
+ */
9
+ import { mkdirSync, readFileSync, writeFileSync, renameSync } from 'node:fs';
10
+ import { dirname } from 'node:path';
11
+ import { randomBytes, createHash } from 'node:crypto';
12
+ import { parseSchedulerStore, serializeSchedulerStore } from './scheduler-store.js';
13
+ import { stableStringify } from './hash.js';
14
+ /**
15
+ * Load the durable store. A MISSING file (ENOENT) ⇒ `[]` (fail-soft, first-run/never-written case); any OTHER
16
+ * read failure (EACCES/EIO/EISDIR/…) RETHROWS — only absence means "empty", not "unreadable for any reason"
17
+ * (a permissions/IO fault silently treated as [] would let a caller-side save() overwrite real on-disk data with
18
+ * an empty store). The pure parser drops corrupt individual records and preserves well-formed ones
19
+ * (forward-compatible) — that degradation stays scoped to malformed CONTENT, not to read failures.
20
+ */
21
+ export function loadSchedulerStore(path) {
22
+ try {
23
+ return parseSchedulerStore(readFileSync(path, 'utf-8'));
24
+ }
25
+ catch (e) {
26
+ if (e.code === 'ENOENT')
27
+ return [];
28
+ throw e;
29
+ }
30
+ }
31
+ /**
32
+ * Persist ATOMICALLY: `mkdir -p` the parent, write a uniquely-named `.tmp` sibling (mode 0o600 — owner-only, the
33
+ * records may carry principal/session identifiers), then `rename` over the target (atomic on POSIX, so a
34
+ * concurrent reader never sees a partial file). Throws on IO failure (caller maps to a domain error).
35
+ */
36
+ export function saveSchedulerStore(path, records) {
37
+ mkdirSync(dirname(path), { recursive: true });
38
+ const tmp = `${path}.${randomBytes(6).toString('hex')}.tmp`;
39
+ writeFileSync(tmp, serializeSchedulerStore(records), { mode: 0o600 });
40
+ renameSync(tmp, path); // atomic over the target
41
+ }
42
+ /** A filesystem-backed {@link SchedulerStore} bound to a path (convenience for the daemon/backend constructors). */
43
+ export function fileSchedulerStore(path) {
44
+ return {
45
+ load: () => loadSchedulerStore(path),
46
+ save: (records) => saveSchedulerStore(path, records),
47
+ };
48
+ }
49
+ /**
50
+ * The ONE cross-process mutation primitive for the shared scheduler store (board [1001]①/[1003]② —
51
+ * server L1-2 + shell 对抗评审 F1 双向合流后的契约层统一原语). Every WRITER (engine backend, resident
52
+ * daemon fire/reap, session reap, sweeps) must go through this instead of hand-rolled
53
+ * load→filter→save — the naked sequence is a lost-update race: two writers load the same base
54
+ * array, mutate independently, and the later `save` silently reverts the earlier one.
55
+ *
56
+ * Optimistic concurrency, format-preserving (no envelope/sidecar/lock file):
57
+ * 1. read the RAW file text and keep it as the base witness (missing file ⇒ empty witness);
58
+ * 2. run `fn(records)` — MUST be pure/replayable; return the next records array, or `null` to
59
+ * abort with no write (e.g. "nothing to do" discovered under the latest base);
60
+ * 3. re-read the raw text just before committing — if it no longer equals the witness, a
61
+ * concurrent writer landed: REPLAY from step 1 (bounded by `retries`, default 5);
62
+ * 4. commit via the same tmp+rename atomic write as {@link saveSchedulerStore}.
63
+ *
64
+ * Honest limits (declared in [1003], server ACK'd): the witness-check→rename window is not zero —
65
+ * this SHRINKS the race from "the whole read-modify-write span" to microseconds, it does not
66
+ * eliminate it (that would need an advisory lock protocol every writer joins; rejected for
67
+ * stale-lock ops burden + win32 friction). `fn` re-runs on replay: side effects inside `fn` are a
68
+ * caller bug.
69
+ */
70
+ export function mutateSchedulerStore(path, fn, opts) {
71
+ const maxRetries = Math.max(0, opts?.retries ?? 5);
72
+ const rawOf = () => {
73
+ try {
74
+ return readFileSync(path, 'utf-8');
75
+ }
76
+ catch (e) {
77
+ if (e.code === 'ENOENT')
78
+ return null; // missing file — parse side treats as []
79
+ throw e; // non-ENOENT (EACCES/EIO/EISDIR/…) must NOT collapse to "missing" — that would hand fn a fake
80
+ // empty baseline and the subsequent save() would overwrite real on-disk data with just fn's additions.
81
+ }
82
+ };
83
+ for (let attempt = 0;; attempt++) {
84
+ const baseRaw = rawOf();
85
+ const records = baseRaw === null ? [] : parseSchedulerStore(baseRaw);
86
+ const next = fn(records);
87
+ if (next === null)
88
+ return { ok: false, reason: 'aborted' };
89
+ if (rawOf() !== baseRaw) {
90
+ if (attempt >= maxRetries)
91
+ return { ok: false, reason: 'conflict_exhausted', retries: attempt };
92
+ continue; // a concurrent writer landed between our read and commit intent — replay on the new base
93
+ }
94
+ saveSchedulerStore(path, next);
95
+ return { ok: true, records: next, retries: attempt };
96
+ }
97
+ }
98
+ /** [1057]① prompts contentDigest(node 绑定;browser-safe 纯层不带 crypto):
99
+ * canonical JSON(stableStringify 同姿势:键序稳定+深度护栏)全量 sha256,**非盐化**
100
+ * ([1050]② operator 对账位——center 产、server 透传、core manifest 归因三方可独立复算)。
101
+ * 入参=下发形的 {sections, scenarioOverrides?}(packId/contentDigest 自身不入 hash)。 */
102
+ export function promptsContentDigest(payload) {
103
+ const canonical = stableStringify({
104
+ sections: payload.sections,
105
+ ...(payload.scenarioOverrides !== undefined ? { scenarioOverrides: payload.scenarioOverrides } : {}),
106
+ });
107
+ const hex = createHash('sha256').update(canonical, 'utf8').digest('hex');
108
+ const sectionHashes = Array.isArray(payload.sections)
109
+ ? payload.sections.map((s) => {
110
+ const t = s?.text;
111
+ return typeof t === 'string' ? sectionContentHash(t) : undefined;
112
+ })
113
+ : [];
114
+ return { contentDigest: `sha256:${hex}`, packId: `center:${hex.slice(0, 12)}`, sectionHashes };
115
+ }
116
+ /** 逐 section 对账锚(core 1.315 `PromptTextDeclaration.contentHash` 同刻度):sha256(text)。 */
117
+ export function sectionContentHash(text) {
118
+ return `sha256:${createHash('sha256').update(text, 'utf8').digest('hex')}`;
119
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * @sema-agent/settings-schema/scheduler-store — the PURE, dep-free CONTRACT for the self-wake scheduler store
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
+ */
20
+ /** A scheduled `when` — structurally identical to core `ScheduledIntent['when']`, inlined (contract stands alone). */
21
+ export type SchedulerWhen = {
22
+ kind: 'cron';
23
+ expr: string;
24
+ } | {
25
+ kind: 'at';
26
+ atMs: number;
27
+ } | {
28
+ kind: 'delay';
29
+ delaySec: number;
30
+ };
31
+ /**
32
+ * A persisted scheduled record (durable across restarts). Required: id/scope/prompt/when/createdMs. Optional
33
+ * fields are additive — a future field is a non-breaking extension (readers preserve unknown fields on round-trip).
34
+ */
35
+ export interface SchedulerRecord {
36
+ id: string;
37
+ scope: string;
38
+ /** 🔴 pinned from ctx (Runner-held), never the model; passed through to runTask unchanged. */
39
+ principal?: string;
40
+ sessionId?: string;
41
+ prompt: string;
42
+ when: SchedulerWhen;
43
+ label?: string;
44
+ createdMs: number;
45
+ nextRunMs?: number;
46
+ }
47
+ /**
48
+ * The store ABSTRACTION the two sides depend on (writer saves, reader loads). The `@sema-agent/settings-schema/node`
49
+ * file binding implements it; a server with a DB backend could implement it differently against the same shape.
50
+ */
51
+ export interface SchedulerStore {
52
+ load(): SchedulerRecord[];
53
+ save(records: SchedulerRecord[]): void;
54
+ }
55
+ /** Structural validation of one record (required fields + a well-formed `when`). Unknown fields are allowed. */
56
+ export declare function isSchedulerRecord(v: unknown): v is SchedulerRecord;
57
+ /** One record dropped by {@link parseSchedulerStore} for failing {@link isSchedulerRecord} (malformed shape).
58
+ * `index` = its position in the parsed array (for correlating with the source file); `record` = the raw
59
+ * (unvalidated) value, for diagnostics. Additive — passing `onWarning` is optional. */
60
+ export type SchedulerParseWarning = {
61
+ index: number;
62
+ kind: 'malformed-record';
63
+ record: unknown;
64
+ };
65
+ export declare function parseSchedulerStore(text: string, onWarning?: (w: SchedulerParseWarning) => void): SchedulerRecord[];
66
+ /** Serialize the store to its canonical on-disk form (stable, pretty JSON). The WRITE half of the format contract. */
67
+ export declare function serializeSchedulerStore(records: SchedulerRecord[]): string;