@xemahq/kernel-contracts 0.41.0 → 0.43.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 (79) hide show
  1. package/dist/agent-composition/lib/model-resolution-matrix.d.ts +5 -2
  2. package/dist/agent-composition/lib/model-resolution-matrix.d.ts.map +1 -1
  3. package/dist/agent-composition/lib/model-resolution-matrix.js +1 -0
  4. package/dist/agent-composition/lib/model-resolution-matrix.js.map +1 -1
  5. package/dist/contribution/lib/contribution-kind.d.ts +2 -1
  6. package/dist/contribution/lib/contribution-kind.d.ts.map +1 -1
  7. package/dist/contribution/lib/contribution-kind.js +1 -0
  8. package/dist/contribution/lib/contribution-kind.js.map +1 -1
  9. package/dist/ingestion-source/index.d.ts +2 -0
  10. package/dist/ingestion-source/index.d.ts.map +1 -0
  11. package/dist/ingestion-source/index.js +18 -0
  12. package/dist/ingestion-source/index.js.map +1 -0
  13. package/dist/ingestion-source/lib/ingestion-source-contribution.d.ts +62 -0
  14. package/dist/ingestion-source/lib/ingestion-source-contribution.d.ts.map +1 -0
  15. package/dist/ingestion-source/lib/ingestion-source-contribution.js +78 -0
  16. package/dist/ingestion-source/lib/ingestion-source-contribution.js.map +1 -0
  17. package/dist/llm-gateway/lib/errors.d.ts +5 -0
  18. package/dist/llm-gateway/lib/errors.d.ts.map +1 -1
  19. package/dist/llm-gateway/lib/errors.js +21 -1
  20. package/dist/llm-gateway/lib/errors.js.map +1 -1
  21. package/dist/output/index.d.ts +1 -0
  22. package/dist/output/index.d.ts.map +1 -1
  23. package/dist/output/index.js +1 -0
  24. package/dist/output/index.js.map +1 -1
  25. package/dist/output/lib/output-version-dto.d.ts +2 -0
  26. package/dist/output/lib/output-version-dto.d.ts.map +1 -1
  27. package/dist/output/lib/point-of-attention.d.ts +19 -0
  28. package/dist/output/lib/point-of-attention.d.ts.map +1 -0
  29. package/dist/output/lib/point-of-attention.js +24 -0
  30. package/dist/output/lib/point-of-attention.js.map +1 -0
  31. package/dist/worker-runtime/index.d.ts +1 -0
  32. package/dist/worker-runtime/index.d.ts.map +1 -1
  33. package/dist/worker-runtime/index.js +1 -0
  34. package/dist/worker-runtime/index.js.map +1 -1
  35. package/dist/worker-runtime/lib/enums.d.ts +5 -0
  36. package/dist/worker-runtime/lib/enums.d.ts.map +1 -1
  37. package/dist/worker-runtime/lib/enums.js +7 -1
  38. package/dist/worker-runtime/lib/enums.js.map +1 -1
  39. package/dist/worker-runtime/lib/runtime-layout-profile.d.ts +4 -1
  40. package/dist/worker-runtime/lib/runtime-layout-profile.d.ts.map +1 -1
  41. package/dist/worker-runtime/lib/runtime-layout-profile.js +12 -0
  42. package/dist/worker-runtime/lib/runtime-layout-profile.js.map +1 -1
  43. package/dist/worker-runtime/lib/workspace-sandbox.d.ts +7 -0
  44. package/dist/worker-runtime/lib/workspace-sandbox.d.ts.map +1 -0
  45. package/dist/worker-runtime/lib/workspace-sandbox.js +36 -0
  46. package/dist/worker-runtime/lib/workspace-sandbox.js.map +1 -0
  47. package/dist/workflow/index.d.ts +1 -0
  48. package/dist/workflow/index.d.ts.map +1 -1
  49. package/dist/workflow/index.js +1 -0
  50. package/dist/workflow/index.js.map +1 -1
  51. package/dist/workflow/lib/enums.d.ts +1 -0
  52. package/dist/workflow/lib/enums.d.ts.map +1 -1
  53. package/dist/workflow/lib/enums.js +1 -0
  54. package/dist/workflow/lib/enums.js.map +1 -1
  55. package/dist/workflow/lib/model-ref.d.ts +17 -0
  56. package/dist/workflow/lib/model-ref.d.ts.map +1 -1
  57. package/dist/workflow/lib/model-ref.js +19 -1
  58. package/dist/workflow/lib/model-ref.js.map +1 -1
  59. package/dist/workflow/lib/workflow-control.d.ts +121 -0
  60. package/dist/workflow/lib/workflow-control.d.ts.map +1 -0
  61. package/dist/workflow/lib/workflow-control.js +101 -0
  62. package/dist/workflow/lib/workflow-control.js.map +1 -0
  63. package/package.json +1 -1
  64. package/src/agent-composition/lib/model-resolution-matrix.ts +16 -1
  65. package/src/contribution/lib/contribution-kind.ts +18 -0
  66. package/src/ingestion-source/index.ts +20 -0
  67. package/src/ingestion-source/lib/ingestion-source-contribution.ts +238 -0
  68. package/src/llm-gateway/lib/errors.ts +46 -0
  69. package/src/output/index.ts +1 -0
  70. package/src/output/lib/output-version-dto.ts +9 -0
  71. package/src/output/lib/point-of-attention.ts +69 -0
  72. package/src/worker-runtime/index.ts +1 -0
  73. package/src/worker-runtime/lib/enums.ts +29 -0
  74. package/src/worker-runtime/lib/runtime-layout-profile.ts +61 -1
  75. package/src/worker-runtime/lib/workspace-sandbox.ts +129 -0
  76. package/src/workflow/index.ts +1 -0
  77. package/src/workflow/lib/enums.ts +8 -0
  78. package/src/workflow/lib/model-ref.ts +45 -0
  79. package/src/workflow/lib/workflow-control.ts +234 -0
@@ -0,0 +1,238 @@
1
+ import { z } from 'zod';
2
+
3
+ /**
4
+ * Maximum length of the short identifier / label / tag strings on an ingestion
5
+ * source manifest. Bounded so list/describe responses stay small and persisted
6
+ * rows have a predictable upper size.
7
+ */
8
+ const INGESTION_SOURCE_STRING_MAX = 200;
9
+
10
+ /**
11
+ * Maximum length of the free-text summary. A summary is a one-liner, not a
12
+ * document.
13
+ */
14
+ const INGESTION_SOURCE_SUMMARY_MAX = 1000;
15
+
16
+ /**
17
+ * Maximum length of a fetch URL / cadence expression / record selector — each
18
+ * is a small path/pattern, never a document.
19
+ */
20
+ const INGESTION_SOURCE_URL_MAX = 2000;
21
+ const INGESTION_SOURCE_EXPRESSION_MAX = 200;
22
+ const INGESTION_SOURCE_SELECTOR_MAX = 500;
23
+
24
+ /** Hard cap on the number of static request headers a source may declare. */
25
+ const INGESTION_SOURCE_HEADERS_MAX = 50;
26
+
27
+ /**
28
+ * HTTP method used to pull from an ingestion source. Closed set — a source
29
+ * either GETs a resource or POSTs a query body.
30
+ */
31
+ export enum IngestionFetchMethod {
32
+ GET = 'GET',
33
+ POST = 'POST',
34
+ }
35
+
36
+ export const INGESTION_FETCH_METHOD_VALUES: readonly IngestionFetchMethod[] = [
37
+ IngestionFetchMethod.GET,
38
+ IngestionFetchMethod.POST,
39
+ ];
40
+
41
+ export const IngestionFetchMethodSchema = z.enum(
42
+ INGESTION_FETCH_METHOD_VALUES as readonly [
43
+ IngestionFetchMethod,
44
+ ...IngestionFetchMethod[],
45
+ ],
46
+ );
47
+
48
+ /**
49
+ * How the pull cadence is expressed. Closed set: a fixed interval or a cron
50
+ * expression. The `expression` field carries the concrete value (a duration
51
+ * string for `interval`, a cron string for `cron`).
52
+ */
53
+ export enum IngestionCadenceKind {
54
+ INTERVAL = 'interval',
55
+ CRON = 'cron',
56
+ }
57
+
58
+ export const INGESTION_CADENCE_KIND_VALUES: readonly IngestionCadenceKind[] = [
59
+ IngestionCadenceKind.INTERVAL,
60
+ IngestionCadenceKind.CRON,
61
+ ];
62
+
63
+ export const IngestionCadenceKindSchema = z.enum(
64
+ INGESTION_CADENCE_KIND_VALUES as readonly [
65
+ IngestionCadenceKind,
66
+ ...IngestionCadenceKind[],
67
+ ],
68
+ );
69
+
70
+ /**
71
+ * How the platform decides a pulled resource CHANGED since the last pull.
72
+ * Closed set — the source declares which signal is authoritative so the puller
73
+ * never guesses.
74
+ */
75
+ export enum IngestionChangeDetectionStrategy {
76
+ /** Compare the HTTP `ETag` response header. */
77
+ ETAG = 'etag',
78
+ /** Compare the HTTP `Last-Modified` response header. */
79
+ LAST_MODIFIED = 'last-modified',
80
+ /** Hash the fetched body and compare content hashes. */
81
+ CONTENT_HASH = 'content-hash',
82
+ }
83
+
84
+ export const INGESTION_CHANGE_DETECTION_STRATEGY_VALUES: readonly IngestionChangeDetectionStrategy[] =
85
+ [
86
+ IngestionChangeDetectionStrategy.ETAG,
87
+ IngestionChangeDetectionStrategy.LAST_MODIFIED,
88
+ IngestionChangeDetectionStrategy.CONTENT_HASH,
89
+ ];
90
+
91
+ export const IngestionChangeDetectionStrategySchema = z.enum(
92
+ INGESTION_CHANGE_DETECTION_STRATEGY_VALUES as readonly [
93
+ IngestionChangeDetectionStrategy,
94
+ ...IngestionChangeDetectionStrategy[],
95
+ ],
96
+ );
97
+
98
+ /**
99
+ * `IngestionFetchSpec` — WHERE the data comes from and HOW to request it. A
100
+ * plain HTTP pull descriptor: URL, method, and optional static headers. No
101
+ * credential is carried inline — a private source references a broker-resolved
102
+ * credential through the platform, never a secret in the manifest.
103
+ */
104
+ export interface IngestionFetchSpec {
105
+ /** Absolute URL to pull from. ≤ 2000 chars. */
106
+ readonly url: string;
107
+ /** HTTP method. Absent ⇒ `GET`. */
108
+ readonly method?: IngestionFetchMethod;
109
+ /** Static request headers (non-secret). Hard-capped at 50 entries. */
110
+ readonly headers?: Readonly<Record<string, string>>;
111
+ }
112
+
113
+ export const IngestionFetchSpecSchema: z.ZodType<IngestionFetchSpec> = z.object({
114
+ url: z.string().min(1).max(INGESTION_SOURCE_URL_MAX),
115
+ method: IngestionFetchMethodSchema.optional(),
116
+ headers: z
117
+ .record(
118
+ z.string().min(1).max(INGESTION_SOURCE_STRING_MAX),
119
+ z.string().max(INGESTION_SOURCE_STRING_MAX),
120
+ )
121
+ .refine((h) => Object.keys(h).length <= INGESTION_SOURCE_HEADERS_MAX, {
122
+ message: `at most ${INGESTION_SOURCE_HEADERS_MAX} headers`,
123
+ })
124
+ .optional(),
125
+ }) as z.ZodType<IngestionFetchSpec>;
126
+
127
+ /**
128
+ * `IngestionCadenceSpec` — HOW OFTEN the source is pulled. `kind` selects the
129
+ * grammar; `expression` carries the concrete value.
130
+ */
131
+ export interface IngestionCadenceSpec {
132
+ readonly kind: IngestionCadenceKind;
133
+ /** Interval duration (for `interval`) or cron expression (for `cron`). */
134
+ readonly expression: string;
135
+ }
136
+
137
+ export const IngestionCadenceSpecSchema: z.ZodType<IngestionCadenceSpec> =
138
+ z.object({
139
+ kind: IngestionCadenceKindSchema,
140
+ expression: z.string().min(1).max(INGESTION_SOURCE_EXPRESSION_MAX),
141
+ }) as z.ZodType<IngestionCadenceSpec>;
142
+
143
+ /**
144
+ * `IngestionChangeDetectionSpec` — HOW the puller decides a resource changed.
145
+ */
146
+ export interface IngestionChangeDetectionSpec {
147
+ readonly strategy: IngestionChangeDetectionStrategy;
148
+ }
149
+
150
+ export const IngestionChangeDetectionSpecSchema: z.ZodType<IngestionChangeDetectionSpec> =
151
+ z.object({
152
+ strategy: IngestionChangeDetectionStrategySchema,
153
+ }) as z.ZodType<IngestionChangeDetectionSpec>;
154
+
155
+ /**
156
+ * `IngestionCatalogSpec` — WHERE pulled records land and how they are tagged
157
+ * for provenance. `targetIndex` is the local catalog/index the records are
158
+ * written into; `provenanceTag` stamps each record so a downstream reader can
159
+ * trace it back to this source.
160
+ */
161
+ export interface IngestionCatalogSpec {
162
+ /** The local catalog/index pulled records are written into. ≤ 200 chars. */
163
+ readonly targetIndex: string;
164
+ /** Provenance tag stamped on every pulled record. ≤ 200 chars. */
165
+ readonly provenanceTag: string;
166
+ }
167
+
168
+ export const IngestionCatalogSpecSchema: z.ZodType<IngestionCatalogSpec> =
169
+ z.object({
170
+ targetIndex: z.string().min(1).max(INGESTION_SOURCE_STRING_MAX),
171
+ provenanceTag: z.string().min(1).max(INGESTION_SOURCE_STRING_MAX),
172
+ }) as z.ZodType<IngestionCatalogSpec>;
173
+
174
+ /**
175
+ * Manifest shape carried by every `ContributionKind.IngestionSource` envelope.
176
+ * One contribution = one PULL-BASED external data source.
177
+ *
178
+ * Mirrors `CapabilityContributionManifest` / `SearchTypeContributionManifest`
179
+ * precisely: a value-identical subset of the wire body the owning ingestion
180
+ * service accepts, validated fail-fast at biome-discovery time. Provenance
181
+ * (`biome.id`, `biome.version`) is NOT a field here — the parser stamps it from
182
+ * the discovering biome's `xema-biome.json`. Authors do not declare provenance
183
+ * inline.
184
+ *
185
+ * The descriptor is pure DATA: it declares WHERE the data comes from
186
+ * (`fetch`), HOW OFTEN to pull it (`cadence`), HOW to detect change
187
+ * (`changeDetection`), WHERE records land (`catalog`), and OPTIONALLY how to
188
+ * extract records from the fetched body (`recordSelector`). It never carries
189
+ * transform/mint code and never carries a secret.
190
+ *
191
+ * Validation is fail-fast: a contribution that violates any constraint is
192
+ * rejected at boot. No silent coercion.
193
+ */
194
+ export interface IngestionSourceContributionManifest {
195
+ /** Stable identifier for this source within its owning biome. ≤ 200 chars. */
196
+ readonly sourceKey: string;
197
+ /** Human-readable source title. ≤ 200 chars. */
198
+ readonly title: string;
199
+ /** Optional one-line summary. ≤ 1000 chars. */
200
+ readonly summary?: string;
201
+ /** WHERE the data comes from and HOW to request it. */
202
+ readonly fetch: IngestionFetchSpec;
203
+ /** HOW OFTEN the source is pulled. */
204
+ readonly cadence: IngestionCadenceSpec;
205
+ /** HOW the puller decides a resource changed since the last pull. */
206
+ readonly changeDetection: IngestionChangeDetectionSpec;
207
+ /** WHERE pulled records land and how they are tagged for provenance. */
208
+ readonly catalog: IngestionCatalogSpec;
209
+ /**
210
+ * Optional JSONPath-ish selector applied to the fetched body to extract the
211
+ * record array. Absent ⇒ the whole body is treated as a single record.
212
+ * ≤ 500 chars.
213
+ */
214
+ readonly recordSelector?: string;
215
+ }
216
+
217
+ /**
218
+ * Zod validator for `IngestionSourceContributionManifest`. The biome-discovery
219
+ * parser AND the owning ingestion service's contribution-sync handler BOTH
220
+ * route every candidate envelope through this schema, so a malformed
221
+ * contribution fails at the boundary instead of at the wire (mirrors
222
+ * `CapabilityContributionManifestSchema`).
223
+ */
224
+ export const IngestionSourceContributionManifestSchema: z.ZodType<IngestionSourceContributionManifest> =
225
+ z.object({
226
+ sourceKey: z.string().min(1).max(INGESTION_SOURCE_STRING_MAX),
227
+ title: z.string().min(1).max(INGESTION_SOURCE_STRING_MAX),
228
+ summary: z.string().min(1).max(INGESTION_SOURCE_SUMMARY_MAX).optional(),
229
+ fetch: IngestionFetchSpecSchema,
230
+ cadence: IngestionCadenceSpecSchema,
231
+ changeDetection: IngestionChangeDetectionSpecSchema,
232
+ catalog: IngestionCatalogSpecSchema,
233
+ recordSelector: z
234
+ .string()
235
+ .min(1)
236
+ .max(INGESTION_SOURCE_SELECTOR_MAX)
237
+ .optional(),
238
+ }) as z.ZodType<IngestionSourceContributionManifest>;
@@ -77,6 +77,52 @@ export interface LlmGatewayErrorFrame {
77
77
  error: LlmGatewayErrorPayload;
78
78
  }
79
79
 
80
+ /**
81
+ * Message signatures that identify a context-window overflow when NO typed
82
+ * {@link LlmGatewayErrorCode} survived to the consumer.
83
+ *
84
+ * The authoritative provider-message → code classification lives in
85
+ * llm-gateway-api's `upstream-error-classifier.ts` (an independent submodule
86
+ * that cannot import this kernel package). This list is the CONSUMER-side
87
+ * counterpart: when an agentic runtime (e.g. OpenCode) swallows the gateway's
88
+ * typed code and re-surfaces only a human-readable string, this is the single
89
+ * place a consumer recognizes the overflow. Keep the two lists in parity when
90
+ * a new provider phrasing appears.
91
+ */
92
+ export const CONTEXT_OVERFLOW_MESSAGE_PATTERNS: readonly RegExp[] = [
93
+ /maximum context length is \d+ tokens/i,
94
+ /input length and max_tokens exceed context limit/i,
95
+ /context_length_exceeded/i,
96
+ /context window/i,
97
+ /prompt is too long/i,
98
+ /reduce the length of (?:the )?messages/i,
99
+ /exceed(?:s|ed)?\b[^.]{0,40}\bcontext/i,
100
+ ];
101
+
102
+ /**
103
+ * Deterministically decide whether an error signal is a context-window
104
+ * overflow — the single well-named predicate every consumer uses instead of
105
+ * an ad-hoc string match. Prefers the TYPED code
106
+ * ({@link LlmGatewayErrorCode.CONTEXT_LENGTH_EXCEEDED}); falls back to the
107
+ * centralized {@link CONTEXT_OVERFLOW_MESSAGE_PATTERNS} ONLY when the code did
108
+ * not survive to the consumer (e.g. a runtime that forwards a raw message).
109
+ *
110
+ * Passing NEITHER a code nor a message returns `false` — never a silent true.
111
+ */
112
+ export function isContextOverflowError(input: {
113
+ code?: string | null;
114
+ message?: string | null;
115
+ }): boolean {
116
+ if (input.code === LlmGatewayErrorCode.CONTEXT_LENGTH_EXCEEDED) {
117
+ return true;
118
+ }
119
+ const message = input.message;
120
+ if (typeof message !== 'string' || message.length === 0) {
121
+ return false;
122
+ }
123
+ return CONTEXT_OVERFLOW_MESSAGE_PATTERNS.some((p) => p.test(message));
124
+ }
125
+
80
126
  /**
81
127
  * Type guard for {@link LlmGatewayErrorFrame}.
82
128
  */
@@ -13,6 +13,7 @@
13
13
  // ═══════════════════════════════════════════════════════════════════════════
14
14
 
15
15
  export * from './lib/output-kind';
16
+ export * from './lib/point-of-attention';
16
17
  export * from './lib/output-render-hint';
17
18
  export * from './lib/output-deliverable-expectation';
18
19
  export * from './lib/output-version-dto';
@@ -1,3 +1,5 @@
1
+ import type { PointOfAttention } from './point-of-attention';
2
+
1
3
  /**
2
4
  * One row in the version history of an output. The Outputs panel
3
5
  * renders a dropdown when `versions.length > 1`; clicking a non-current
@@ -18,4 +20,11 @@ export interface OutputVersionDto {
18
20
  readonly producerAgentSlug: string | null;
19
21
  /** True when this row is the latest version. Exactly one row has this set. */
20
22
  readonly isCurrent: boolean;
23
+ /**
24
+ * Generic producer-authored highlights for THIS version — the salient
25
+ * things a reader should notice (an assumption, a gap, a risk). Optional
26
+ * extensibility escape hatch so the fixed version schema stays open without
27
+ * a per-domain field. Absent/empty ⇒ nothing flagged.
28
+ */
29
+ readonly pointsOfAttention?: readonly PointOfAttention[];
21
30
  }
@@ -0,0 +1,69 @@
1
+ import { z } from 'zod';
2
+
3
+ /**
4
+ * Bounded maxes for the short label / longer detail strings on a point of
5
+ * attention. Kept small so a version row carrying a handful of them stays a
6
+ * cheap, list-safe payload.
7
+ */
8
+ const POINT_OF_ATTENTION_LABEL_MAX = 200;
9
+ const POINT_OF_ATTENTION_DETAIL_MAX = 2000;
10
+
11
+ /**
12
+ * Severity of a {@link PointOfAttention}. Closed set — a consumer renders each
13
+ * value with a fixed badge/colour, so the domain must never be a free-form
14
+ * string.
15
+ */
16
+ export enum PointOfAttentionSeverity {
17
+ /** Neutral highlight — worth surfacing, no action implied. */
18
+ INFO = 'info',
19
+ /** Something the reader should scrutinise before relying on the artifact. */
20
+ WARNING = 'warning',
21
+ /** A blocking concern — the artifact should not be trusted/acted on as-is. */
22
+ CRITICAL = 'critical',
23
+ }
24
+
25
+ export const POINT_OF_ATTENTION_SEVERITY_VALUES: readonly PointOfAttentionSeverity[] =
26
+ [
27
+ PointOfAttentionSeverity.INFO,
28
+ PointOfAttentionSeverity.WARNING,
29
+ PointOfAttentionSeverity.CRITICAL,
30
+ ];
31
+
32
+ /**
33
+ * Zod schema for {@link PointOfAttentionSeverity}. Built from the enum so the
34
+ * literal list lives in exactly one place.
35
+ */
36
+ export const PointOfAttentionSeveritySchema = z.enum(
37
+ POINT_OF_ATTENTION_SEVERITY_VALUES as readonly [
38
+ PointOfAttentionSeverity,
39
+ ...PointOfAttentionSeverity[],
40
+ ],
41
+ );
42
+
43
+ /**
44
+ * A GENERIC "point of attention" / highlight attached to a per-version
45
+ * artifact. This is the deliberate extensibility escape hatch that keeps a
46
+ * fixed artifact schema open: a producer can flag the salient things a reader
47
+ * should notice (an assumption made, a gap, a risk) WITHOUT the kernel growing
48
+ * a domain-specific field for each. Domain-agnostic — carries only a label, a
49
+ * detail, and a {@link PointOfAttentionSeverity}; the meaning is the
50
+ * producer's, never the kernel's.
51
+ */
52
+ export interface PointOfAttention {
53
+ /** Short headline for the point (≤ 200 chars). */
54
+ readonly label: string;
55
+ /** Longer human-readable explanation (≤ 2000 chars). */
56
+ readonly detail: string;
57
+ /** How much scrutiny this point warrants. */
58
+ readonly severity: PointOfAttentionSeverity;
59
+ }
60
+
61
+ /**
62
+ * Zod validator for {@link PointOfAttention}. Fail-fast: a malformed point is
63
+ * rejected at the boundary rather than silently coerced.
64
+ */
65
+ export const PointOfAttentionSchema: z.ZodType<PointOfAttention> = z.object({
66
+ label: z.string().min(1).max(POINT_OF_ATTENTION_LABEL_MAX),
67
+ detail: z.string().min(1).max(POINT_OF_ATTENTION_DETAIL_MAX),
68
+ severity: PointOfAttentionSeveritySchema,
69
+ }) as z.ZodType<PointOfAttention>;
@@ -12,4 +12,5 @@ export * from './lib/capabilities';
12
12
  export * from './lib/messages';
13
13
  export * from './lib/runtime';
14
14
  export * from './lib/runtime-layout-profile';
15
+ export * from './lib/workspace-sandbox';
15
16
  export * from './lib/schemas';
@@ -26,6 +26,35 @@ export enum WorkerRuntimeKind {
26
26
  Hermes = 'hermes',
27
27
  }
28
28
 
29
+ /**
30
+ * How a runtime consumes a per-skill "slash command" launcher file, if at
31
+ * all. Closed set — a runtime that grows a new command surface adds a member
32
+ * here and the composer/proxy switch exhaustively on it.
33
+ *
34
+ * The platform composes ONE per-skill launch command (markdown body with a
35
+ * `$ARGUMENTS` placeholder) so typing `/<skill>` loads + applies that skill.
36
+ * WHETHER and in WHICH FORMAT a runtime reads that file differs:
37
+ *
38
+ * - `Markdown` — the runtime reads markdown command files from its
39
+ * `commandsDir` (OpenCode: `.opencode/{command,commands}/**.md`; Claude
40
+ * Code: `.claude/commands/*.md`). Xema emits the launcher for these.
41
+ * - `Toml` — the runtime's custom commands are TOML, NOT markdown (Qwen).
42
+ * Xema does NOT synthesise TOML skill launchers — skills launch via the
43
+ * runtime's own `/skills <name>` surface / model-invoked skill tool — so
44
+ * the platform emits NOTHING for a TOML runtime rather than a file the
45
+ * runtime silently ignores.
46
+ * - `None` — the runtime has no file-based slash-command surface at all
47
+ * (Hermes: `commandsDir` is `null`). Emit nothing.
48
+ *
49
+ * Only `Markdown` runtimes receive a materialised per-skill command file;
50
+ * `Toml`/`None` receive none (never a stray, silently-ignored file).
51
+ */
52
+ export enum RuntimeCommandFormat {
53
+ Markdown = 'markdown',
54
+ Toml = 'toml',
55
+ None = 'none',
56
+ }
57
+
29
58
  /**
30
59
  * Lifecycle hook surfaces that a runtime may support. The set is closed —
31
60
  * adding a new hook is a kernel change.
@@ -20,7 +20,7 @@
20
20
  // is where the platform renders the system overlay per WS7.
21
21
  // ═══════════════════════════════════════════════════════════════════════════
22
22
 
23
- import { WorkerRuntimeKind } from './enums';
23
+ import { RuntimeCommandFormat, WorkerRuntimeKind } from './enums';
24
24
 
25
25
  /**
26
26
  * The workspace slots whose on-disk location differs per orchestrator. The
@@ -48,6 +48,43 @@ export interface RuntimeLayoutProfile {
48
48
  * fail-fast contract as `agentBundlesDir`.
49
49
  */
50
50
  readonly commandsDir: string | null;
51
+ /**
52
+ * How this runtime consumes a per-skill `/`-launch command — the capability
53
+ * the composer/proxy switch on to decide WHETHER (and in which format) to
54
+ * materialise a launcher file. `Markdown` runtimes get a launcher written at
55
+ * `commandsDir`; `Toml`/`None` runtimes get NOTHING (skills still mount as
56
+ * bundles and launch via the runtime's own `/skills` surface / skill tool),
57
+ * so no stray file lands where the runtime would silently ignore it.
58
+ *
59
+ * INVARIANT: `commandFormat === Markdown` ⟺ `commandsDir !== null`. A runtime
60
+ * with no command surface (`commandsDir: null`) MUST declare `None`.
61
+ */
62
+ readonly commandFormat: RuntimeCommandFormat;
63
+ /**
64
+ * Absolute path to the runtime's PRIMARY config file — the file the
65
+ * orchestrator's bundle composer renders and the supervisor materialises on
66
+ * the PVC. It carries the LLM-gateway credential (OpenCode reads
67
+ * `.opencode/opencode.jsonc`; Qwen `.qwen/settings.json` → `env.OPENAI_API_KEY`;
68
+ * Claude `.claude/settings.json` → `env.ANTHROPIC_AUTH_TOKEN`; Hermes
69
+ * `.hermes/config.json` → `provider.api_key`) and/or merged MCP bearer headers.
70
+ *
71
+ * This is the single source of truth for the config-file location: both the
72
+ * proxy's on-disk layout and the workspace-sandbox protected-path contract
73
+ * (`deriveWorkspaceSandboxProtection`) source it here rather than each
74
+ * re-deriving a per-runtime basename literal.
75
+ */
76
+ readonly runtimeConfigFile: string;
77
+ /**
78
+ * Absolute path to a DEDICATED MCP-server file the runtime reads separately
79
+ * from its primary config (Claude Code: `.mcp.json` at the workspace root),
80
+ * or `null` when the runtime merges MCP into {@link runtimeConfigFile}
81
+ * (OpenCode/Qwen/Hermes: `InConfig`). When non-null the file carries per-server
82
+ * bearer headers and is therefore part of the secret read-deny set.
83
+ *
84
+ * INVARIANT: `mcpConfigFile !== null` ⟺ the runtime places MCP in a separate
85
+ * file. A runtime that folds MCP into its primary config MUST declare `null`.
86
+ */
87
+ readonly mcpConfigFile: string | null;
51
88
  }
52
89
 
53
90
  /**
@@ -63,6 +100,11 @@ export const RUNTIME_LAYOUT_PROFILES: Readonly<
63
100
  contextFile: '/workspace/AGENTS.md',
64
101
  agentBundlesDir: '/workspace/.opencode/agents',
65
102
  commandsDir: '/workspace/.opencode/command',
103
+ // OpenCode reads markdown commands from `{command,commands}/**.md`.
104
+ commandFormat: RuntimeCommandFormat.Markdown,
105
+ runtimeConfigFile: '/workspace/.opencode/opencode.jsonc',
106
+ // OpenCode pre-merges MCP into opencode.jsonc (`InConfig`) — no separate file.
107
+ mcpConfigFile: null,
66
108
  },
67
109
  [WorkerRuntimeKind.Qwen]: {
68
110
  kind: WorkerRuntimeKind.Qwen,
@@ -70,6 +112,13 @@ export const RUNTIME_LAYOUT_PROFILES: Readonly<
70
112
  contextFile: '/workspace/AGENTS.md',
71
113
  agentBundlesDir: '/workspace/.qwen/agents',
72
114
  commandsDir: '/workspace/.qwen/commands',
115
+ // Qwen custom commands are TOML, not markdown; skills launch via Qwen's
116
+ // own `/skills <name>` surface. A synthesised markdown `.md` under
117
+ // `.qwen/commands` would be silently ignored — so Xema emits NOTHING.
118
+ commandFormat: RuntimeCommandFormat.Toml,
119
+ runtimeConfigFile: '/workspace/.qwen/settings.json',
120
+ // Qwen reads its `mcpServers` map INSIDE `.qwen/settings.json` (`InConfig`).
121
+ mcpConfigFile: null,
73
122
  },
74
123
  [WorkerRuntimeKind.ClaudeCode]: {
75
124
  kind: WorkerRuntimeKind.ClaudeCode,
@@ -77,6 +126,12 @@ export const RUNTIME_LAYOUT_PROFILES: Readonly<
77
126
  contextFile: '/workspace/CLAUDE.md',
78
127
  agentBundlesDir: '/workspace/.claude/agents',
79
128
  commandsDir: '/workspace/.claude/commands',
129
+ // Claude Code custom slash-commands ARE markdown, under `.claude/commands`.
130
+ commandFormat: RuntimeCommandFormat.Markdown,
131
+ runtimeConfigFile: '/workspace/.claude/settings.json',
132
+ // Claude Code reads project-scoped MCP from a DEDICATED `.mcp.json` at the
133
+ // workspace root (SeparateFile) — NOT from `.claude/settings.json`.
134
+ mcpConfigFile: '/workspace/.mcp.json',
80
135
  },
81
136
  [WorkerRuntimeKind.Hermes]: {
82
137
  kind: WorkerRuntimeKind.Hermes,
@@ -89,6 +144,11 @@ export const RUNTIME_LAYOUT_PROFILES: Readonly<
89
144
  // is a portable `.md` surface, so the composer must not mount them as files.
90
145
  agentBundlesDir: null,
91
146
  commandsDir: null,
147
+ // No file-based slash-command surface at all — emit nothing.
148
+ commandFormat: RuntimeCommandFormat.None,
149
+ runtimeConfigFile: '/workspace/.hermes/config.json',
150
+ // Hermes reads its `mcpServers` map INSIDE `.hermes/config.json` (`InConfig`).
151
+ mcpConfigFile: null,
92
152
  },
93
153
  };
94
154
 
@@ -0,0 +1,129 @@
1
+ // ═══════════════════════════════════════════════════════════════════════════
2
+ // ── Workspace-sandbox protection contract ──
3
+ //
4
+ // A FIRST-CLASS, per-runtime security invariant for the agent-workspace sandbox.
5
+ // The agent runs a code CLI (OpenCode/Qwen/Claude/Hermes) inside the workspace
6
+ // pod with its OWN file tools (read/edit/write). Those tools MUST NOT be able to:
7
+ //
8
+ // (a) READ the mounted platform secrets — the runtime config file carries the
9
+ // LLM-gateway credential (a signed JWT) and/or merged MCP bearer headers,
10
+ // and a dedicated `.mcp.json` (Claude) carries per-server bearer headers.
11
+ // (b) MUTATE the agent's OWN system-instructions / agent-definition bundles —
12
+ // the auto-loaded context file (`AGENTS.md`/`CLAUDE.md`), the file-based
13
+ // sub-agent definitions, the Xema-private system overlay + injected
14
+ // instruction sections, and the `context.json` capability-authority spec
15
+ // (rewriting it would let the agent self-grant tools).
16
+ //
17
+ // This module DECLARES that protected-path set ONCE, derived from the kernel
18
+ // `RuntimeLayoutProfile` (the SSOT for every per-runtime path). Each runtime's
19
+ // config builder translates the neutral glob set into its NATIVE permission
20
+ // vocabulary (OpenCode `permission.read`/`permission.edit` globs; Claude Code
21
+ // `permissions.deny: ["Read(...)","Edit(...)"]`). Runtimes with no per-file
22
+ // permission surface (Qwen/Hermes) cannot consume the deny set and fall back to
23
+ // the pod's uid + per-org mount confinement — see the residual note in each
24
+ // builder. The contract is declared here regardless so the invariant is
25
+ // single-sourced, not scattered across builders.
26
+ //
27
+ // Globs are runtime-syntax-NEUTRAL absolute paths anchored at `/workspace`
28
+ // (`*` matches any character; `<dir>/**` covers a whole slot). Callers adapt
29
+ // them to their runtime's matcher.
30
+ // ═══════════════════════════════════════════════════════════════════════════
31
+
32
+ import {
33
+ WorkspaceMount,
34
+ } from '../../agent-workspace/lib/workspace-layout';
35
+
36
+ import type { RuntimeLayoutProfile } from './runtime-layout-profile';
37
+
38
+ /**
39
+ * The resolved protected-path set for one runtime's workspace sandbox.
40
+ *
41
+ * - `secretReadDenyGlobs` — the agent's READ tool must DENY these (secrets). The
42
+ * agent may still read its own instructions/code; only credential-bearing
43
+ * files are denied.
44
+ * - `protectedEditDenyGlobs` — the agent's EDIT/WRITE tool must DENY these
45
+ * (own system-instructions, agent-definitions, authority spec, and — defense
46
+ * in depth — its own credential config). The agent may still edit workspace
47
+ * code, deliverables, repos, inputs, uploads, and tmp.
48
+ *
49
+ * Every entry is an absolute `/workspace/...` glob. Both sets are de-duplicated
50
+ * and stable-ordered.
51
+ */
52
+ export interface WorkspaceSandboxProtection {
53
+ readonly secretReadDenyGlobs: readonly string[];
54
+ readonly protectedEditDenyGlobs: readonly string[];
55
+ }
56
+
57
+ /**
58
+ * Constant, runtime-AGNOSTIC Xema-private system-instruction + authority slots.
59
+ * These are excluded from the per-runtime `RuntimeLayoutProfile` on purpose (the
60
+ * profile only carries the slots whose ON-DISK path differs per runtime), so
61
+ * they are sourced from the `WorkspaceMount` SSOT here — never re-literalled.
62
+ *
63
+ * All are EDIT-denied (never mutate the injected system overlay / instructions /
64
+ * capability-authority). They are NOT read-denied: the runtime legitimately
65
+ * loads the overlay + instructions as context, and the plugin reads
66
+ * `context.json`.
67
+ */
68
+ const XEMA_PRIVATE_INSTRUCTION_EDIT_GLOBS: readonly string[] = [
69
+ // /workspace/.xema/system-overlay.md — the injected system prompt overlay.
70
+ WorkspaceMount.SystemOverlay,
71
+ // /workspace/.xema/instructions/** — injected rule/policy instruction sections.
72
+ `${WorkspaceMount.Instructions}/**`,
73
+ // /workspace/context.json — carries authority.allowedXemaTools; rewriting it
74
+ // would let the agent escalate its own tool grants.
75
+ WorkspaceMount.ContextJson,
76
+ ];
77
+
78
+ function dedupeStable(globs: readonly string[]): string[] {
79
+ return [...new Set(globs)];
80
+ }
81
+
82
+ /**
83
+ * Derive the workspace-sandbox protected-path set for a runtime, from its
84
+ * kernel `RuntimeLayoutProfile`. Pure — no I/O, no defaults that mask input.
85
+ *
86
+ * The set is CLOSED and fail-closed: a config file always exists (every runtime
87
+ * declares `runtimeConfigFile`), and every credential-bearing / instruction path
88
+ * the profile exposes is protected. A runtime with no file-based sub-agent
89
+ * surface (`agentBundlesDir === null`, e.g. Hermes) contributes no agent-def
90
+ * glob — there is no such directory to protect.
91
+ */
92
+ export function deriveWorkspaceSandboxProtection(
93
+ profile: RuntimeLayoutProfile,
94
+ ): WorkspaceSandboxProtection {
95
+ const configFile = profile.runtimeConfigFile;
96
+ const mcpFile = profile.mcpConfigFile;
97
+
98
+ // ── (a) Secret READ deny ─────────────────────────────────────────────────
99
+ // The credential-bearing config file + (if separate) the bearer-header MCP
100
+ // file. Nothing else is read-denied — the agent must still read its own
101
+ // instructions + workspace code.
102
+ const secretReadDenyGlobs: string[] = [configFile];
103
+ if (mcpFile !== null) {
104
+ secretReadDenyGlobs.push(mcpFile);
105
+ }
106
+
107
+ // ── (b) Own-instruction / agent-def EDIT deny ────────────────────────────
108
+ const protectedEditDenyGlobs: string[] = [
109
+ // The auto-loaded context file (AGENTS.md / CLAUDE.md) IS the agent's own
110
+ // system instructions.
111
+ profile.contextFile,
112
+ // Defense in depth: never let the agent rewrite its own credential config
113
+ // (e.g. to strip its own deny rules or exfiltrate the token).
114
+ configFile,
115
+ ...XEMA_PRIVATE_INSTRUCTION_EDIT_GLOBS,
116
+ ];
117
+ if (mcpFile !== null) {
118
+ protectedEditDenyGlobs.push(mcpFile);
119
+ }
120
+ if (profile.agentBundlesDir !== null) {
121
+ // The whole file-based sub-agent-definition slot.
122
+ protectedEditDenyGlobs.push(`${profile.agentBundlesDir}/**`);
123
+ }
124
+
125
+ return {
126
+ secretReadDenyGlobs: dedupeStable(secretReadDenyGlobs),
127
+ protectedEditDenyGlobs: dedupeStable(protectedEditDenyGlobs),
128
+ };
129
+ }
@@ -8,6 +8,7 @@
8
8
  // ═══════════════════════════════════════════════════════════════════════════
9
9
 
10
10
  export * from './lib/enums';
11
+ export * from './lib/workflow-control';
11
12
  export * from './lib/temporal-namespace';
12
13
  export * from './lib/platform-task-queue';
13
14
  export * from './lib/errors';