@stigmer/sdk 3.2.3 → 3.4.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 (108) hide show
  1. package/__tests__/errors.test.js +44 -1
  2. package/__tests__/errors.test.js.map +1 -1
  3. package/__tests__/manifest.test.d.ts +2 -0
  4. package/__tests__/manifest.test.d.ts.map +1 -0
  5. package/__tests__/manifest.test.js +229 -0
  6. package/__tests__/manifest.test.js.map +1 -0
  7. package/__tests__/session.test.d.ts +6 -0
  8. package/__tests__/session.test.d.ts.map +1 -0
  9. package/__tests__/session.test.js +50 -0
  10. package/__tests__/session.test.js.map +1 -0
  11. package/billing.d.ts +83 -1
  12. package/billing.d.ts.map +1 -1
  13. package/billing.js +91 -1
  14. package/billing.js.map +1 -1
  15. package/cursor-accounts.d.ts +94 -0
  16. package/cursor-accounts.d.ts.map +1 -0
  17. package/cursor-accounts.js +119 -0
  18. package/cursor-accounts.js.map +1 -0
  19. package/errors.d.ts +14 -0
  20. package/errors.d.ts.map +1 -1
  21. package/errors.js +26 -0
  22. package/errors.js.map +1 -1
  23. package/gen/agent.d.ts +5 -0
  24. package/gen/agent.d.ts.map +1 -1
  25. package/gen/agent.js +9 -1
  26. package/gen/agent.js.map +1 -1
  27. package/gen/agentexecution.d.ts +1 -0
  28. package/gen/agentexecution.d.ts.map +1 -1
  29. package/gen/agentexecution.js +2 -0
  30. package/gen/agentexecution.js.map +1 -1
  31. package/gen/agentinstance.d.ts +1 -0
  32. package/gen/agentinstance.d.ts.map +1 -1
  33. package/gen/agentinstance.js +1 -0
  34. package/gen/agentinstance.js.map +1 -1
  35. package/gen/authorization-config.d.ts.map +1 -1
  36. package/gen/authorization-config.js +1 -0
  37. package/gen/authorization-config.js.map +1 -1
  38. package/gen/client.d.ts +6 -2
  39. package/gen/client.d.ts.map +1 -1
  40. package/gen/client.js +4 -0
  41. package/gen/client.js.map +1 -1
  42. package/gen/datastore.d.ts +129 -0
  43. package/gen/datastore.d.ts.map +1 -0
  44. package/gen/datastore.js +303 -0
  45. package/gen/datastore.js.map +1 -0
  46. package/gen/session.d.ts +1 -0
  47. package/gen/session.d.ts.map +1 -1
  48. package/gen/session.js +1 -0
  49. package/gen/session.js.map +1 -1
  50. package/index.d.ts +6 -4
  51. package/index.d.ts.map +1 -1
  52. package/index.js +8 -4
  53. package/index.js.map +1 -1
  54. package/manifest/client.d.ts +64 -0
  55. package/manifest/client.d.ts.map +1 -0
  56. package/manifest/client.js +109 -0
  57. package/manifest/client.js.map +1 -0
  58. package/manifest/index.d.ts +9 -0
  59. package/manifest/index.d.ts.map +1 -0
  60. package/manifest/index.js +11 -0
  61. package/manifest/index.js.map +1 -0
  62. package/manifest/parse.d.ts +57 -0
  63. package/manifest/parse.d.ts.map +1 -0
  64. package/manifest/parse.js +134 -0
  65. package/manifest/parse.js.map +1 -0
  66. package/manifest/redaction.d.ts +16 -0
  67. package/manifest/redaction.d.ts.map +1 -0
  68. package/manifest/redaction.js +19 -0
  69. package/manifest/redaction.js.map +1 -0
  70. package/manifest/registry.d.ts +44 -0
  71. package/manifest/registry.d.ts.map +1 -0
  72. package/manifest/registry.js +178 -0
  73. package/manifest/registry.js.map +1 -0
  74. package/manifest/serialize.d.ts +24 -0
  75. package/manifest/serialize.d.ts.map +1 -0
  76. package/manifest/serialize.js +78 -0
  77. package/manifest/serialize.js.map +1 -0
  78. package/package.json +4 -3
  79. package/session.d.ts +37 -0
  80. package/session.d.ts.map +1 -1
  81. package/session.js +43 -0
  82. package/session.js.map +1 -1
  83. package/src/__tests__/errors.test.ts +58 -0
  84. package/src/__tests__/manifest.test.ts +271 -0
  85. package/src/__tests__/session.test.ts +66 -0
  86. package/src/billing.ts +160 -0
  87. package/src/cursor-accounts.ts +207 -0
  88. package/src/errors.ts +27 -0
  89. package/src/gen/agent.ts +15 -1
  90. package/src/gen/agentexecution.ts +2 -0
  91. package/src/gen/agentinstance.ts +2 -0
  92. package/src/gen/authorization-config.ts +1 -0
  93. package/src/gen/client.ts +7 -2
  94. package/src/gen/datastore.ts +381 -0
  95. package/src/gen/session.ts +2 -0
  96. package/src/index.ts +36 -1
  97. package/src/manifest/client.ts +145 -0
  98. package/src/manifest/index.ts +25 -0
  99. package/src/manifest/parse.ts +204 -0
  100. package/src/manifest/redaction.ts +20 -0
  101. package/src/manifest/registry.ts +232 -0
  102. package/src/manifest/serialize.ts +92 -0
  103. package/src/session.ts +48 -0
  104. package/src/stigmer.ts +8 -0
  105. package/stigmer.d.ts +6 -0
  106. package/stigmer.d.ts.map +1 -1
  107. package/stigmer.js +8 -0
  108. package/stigmer.js.map +1 -1
@@ -0,0 +1,24 @@
1
+ import { type Message } from "@bufbuild/protobuf";
2
+ /**
3
+ * Serialize a resource proto into the canonical, editable Stigmer YAML form.
4
+ *
5
+ * The resource kind is derived from the message's proto type, so any
6
+ * registry-supported resource (an `Agent` from `stigmer.agent.get()`, an
7
+ * `Environment` from `stigmer.environment.get()`, …) serializes with the
8
+ * same call. `metadata.id` is preserved — the output is a full-fidelity
9
+ * representation of the stored resource, minus system-managed state.
10
+ *
11
+ * @param message - A resource proto of a registry-supported kind.
12
+ * @returns YAML text suitable for editing and re-applying.
13
+ * @throws {Error} When the message's kind is not in the manifest registry.
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * import { serializeManifest } from "@stigmer/sdk";
18
+ *
19
+ * const agent = await stigmer.agent.get(agentId);
20
+ * const yaml = serializeManifest(agent);
21
+ * ```
22
+ */
23
+ export declare function serializeManifest(message: Message): string;
24
+ //# sourceMappingURL=serialize.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serialize.d.ts","sourceRoot":"","sources":["../../src/manifest/serialize.ts"],"names":[],"mappings":"AAeA,OAAO,EAAU,KAAK,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAI1D;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CAoC1D"}
@@ -0,0 +1,78 @@
1
+ // Proto → canonical Stigmer YAML serialization.
2
+ //
3
+ // Follows the platform-wide protojson parity contract (Go CLI, TS CLI
4
+ // output/proto.ts, mcp-server marshal.ts): `toJson` with
5
+ // `useProtoFieldName: true` emits snake_case field names and omits unset
6
+ // defaults, so the output round-trips through `parseManifest`'s strict
7
+ // `fromJson` without loss.
8
+ //
9
+ // Two deliberate departures from the raw protojson projection, both for
10
+ // the *editable manifest* use case:
11
+ // - `status` and `metadata.version` are stripped — they are system-managed
12
+ // and would be ignored (or rejected) on apply.
13
+ // - the envelope field is spelled `apiVersion` (the canonical form used in
14
+ // every repo manifest and doc); `fromJson` accepts both spellings.
15
+ import { toJson } from "@bufbuild/protobuf";
16
+ import { stringify as stringifyYaml } from "yaml";
17
+ import { manifestHandlerForTypeName, manifestKinds } from "./registry.js";
18
+ /**
19
+ * Serialize a resource proto into the canonical, editable Stigmer YAML form.
20
+ *
21
+ * The resource kind is derived from the message's proto type, so any
22
+ * registry-supported resource (an `Agent` from `stigmer.agent.get()`, an
23
+ * `Environment` from `stigmer.environment.get()`, …) serializes with the
24
+ * same call. `metadata.id` is preserved — the output is a full-fidelity
25
+ * representation of the stored resource, minus system-managed state.
26
+ *
27
+ * @param message - A resource proto of a registry-supported kind.
28
+ * @returns YAML text suitable for editing and re-applying.
29
+ * @throws {Error} When the message's kind is not in the manifest registry.
30
+ *
31
+ * @example
32
+ * ```ts
33
+ * import { serializeManifest } from "@stigmer/sdk";
34
+ *
35
+ * const agent = await stigmer.agent.get(agentId);
36
+ * const yaml = serializeManifest(agent);
37
+ * ```
38
+ */
39
+ export function serializeManifest(message) {
40
+ const handler = manifestHandlerForTypeName(message.$typeName);
41
+ if (handler === undefined) {
42
+ const supported = manifestKinds()
43
+ .map((h) => h.yamlKind)
44
+ .join(", ");
45
+ throw new Error(`Cannot serialize ${message.$typeName} as a manifest: kind is not ` +
46
+ `in the manifest registry. Supported kinds: ${supported}.`);
47
+ }
48
+ const json = toJson(handler.schema, message, { useProtoFieldName: true });
49
+ if (json === null || typeof json !== "object" || Array.isArray(json)) {
50
+ throw new Error(`Cannot serialize ${handler.displayName}: unexpected non-object ` +
51
+ "protojson projection.");
52
+ }
53
+ const { api_version, kind, status, metadata, ...rest } = json;
54
+ void status;
55
+ const doc = {
56
+ apiVersion: typeof api_version === "string" && api_version !== ""
57
+ ? api_version
58
+ : handler.apiVersion,
59
+ kind: typeof kind === "string" && kind !== "" ? kind : handler.yamlKind,
60
+ ...(isPlainObject(metadata) && { metadata: stripSystemMetadata(metadata) }),
61
+ ...rest,
62
+ };
63
+ // lineWidth: 0 disables wrapping (long instruction lines stay intact);
64
+ // blockQuote: "literal" renders multi-line strings as readable `|` blocks.
65
+ return stringifyYaml(doc, { lineWidth: 0, blockQuote: "literal" });
66
+ }
67
+ // metadata.version (version id, previous-version pointer, tag) is written by
68
+ // the server on every mutation; round-tripping it through an edit would be
69
+ // misleading at best. Everything else — including id — is user-meaningful.
70
+ function stripSystemMetadata(metadata) {
71
+ const { version, ...rest } = metadata;
72
+ void version;
73
+ return rest;
74
+ }
75
+ function isPlainObject(value) {
76
+ return typeof value === "object" && value !== null && !Array.isArray(value);
77
+ }
78
+ //# sourceMappingURL=serialize.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serialize.js","sourceRoot":"","sources":["../../src/manifest/serialize.ts"],"names":[],"mappings":"AAAA,gDAAgD;AAChD,EAAE;AACF,sEAAsE;AACtE,yDAAyD;AACzD,yEAAyE;AACzE,uEAAuE;AACvE,2BAA2B;AAC3B,EAAE;AACF,wEAAwE;AACxE,oCAAoC;AACpC,6EAA6E;AAC7E,mDAAmD;AACnD,6EAA6E;AAC7E,uEAAuE;AAEvE,OAAO,EAAE,MAAM,EAAgB,MAAM,oBAAoB,CAAC;AAC1D,OAAO,EAAE,SAAS,IAAI,aAAa,EAAE,MAAM,MAAM,CAAC;AAClD,OAAO,EAAE,0BAA0B,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAE1E;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,UAAU,iBAAiB,CAAC,OAAgB;IAChD,MAAM,OAAO,GAAG,0BAA0B,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IAC9D,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;QAC1B,MAAM,SAAS,GAAG,aAAa,EAAE;aAC9B,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC;aACtB,IAAI,CAAC,IAAI,CAAC,CAAC;QACd,MAAM,IAAI,KAAK,CACb,oBAAoB,OAAO,CAAC,SAAS,8BAA8B;YACjE,8CAA8C,SAAS,GAAG,CAC7D,CAAC;IACJ,CAAC;IAED,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,EAAE,OAAO,EAAE,EAAE,iBAAiB,EAAE,IAAI,EAAE,CAAC,CAAC;IAC1E,IAAI,IAAI,KAAK,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QACrE,MAAM,IAAI,KAAK,CACb,oBAAoB,OAAO,CAAC,WAAW,0BAA0B;YAC/D,uBAAuB,CAC1B,CAAC;IACJ,CAAC;IAED,MAAM,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,IAAI,EAAE,GACpD,IAA+B,CAAC;IAClC,KAAK,MAAM,CAAC;IAEZ,MAAM,GAAG,GAA4B;QACnC,UAAU,EAAE,OAAO,WAAW,KAAK,QAAQ,IAAI,WAAW,KAAK,EAAE;YAC/D,CAAC,CAAC,WAAW;YACb,CAAC,CAAC,OAAO,CAAC,UAAU;QACtB,IAAI,EAAE,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ;QACvE,GAAG,CAAC,aAAa,CAAC,QAAQ,CAAC,IAAI,EAAE,QAAQ,EAAE,mBAAmB,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC3E,GAAG,IAAI;KACR,CAAC;IAEF,uEAAuE;IACvE,2EAA2E;IAC3E,OAAO,aAAa,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,CAAC,EAAE,UAAU,EAAE,SAAS,EAAE,CAAC,CAAC;AACrE,CAAC;AAED,6EAA6E;AAC7E,2EAA2E;AAC3E,2EAA2E;AAC3E,SAAS,mBAAmB,CAC1B,QAAiC;IAEjC,MAAM,EAAE,OAAO,EAAE,GAAG,IAAI,EAAE,GAAG,QAAQ,CAAC;IACtC,KAAK,OAAO,CAAC;IACb,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAS,aAAa,CAAC,KAAc;IACnC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAC9E,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stigmer/sdk",
3
- "version": "3.2.3",
3
+ "version": "3.4.0",
4
4
  "description": "Stigmer TypeScript SDK — typed API client for all Stigmer platform resources",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -39,11 +39,12 @@
39
39
  },
40
40
  "dependencies": {
41
41
  "@connectrpc/connect": "^2.0.0",
42
- "@connectrpc/connect-web": "^2.0.0"
42
+ "@connectrpc/connect-web": "^2.0.0",
43
+ "yaml": "^2.8.2"
43
44
  },
44
45
  "peerDependencies": {
45
46
  "@bufbuild/protobuf": "^2.0.0",
46
47
  "@connectrpc/connect-node": "^2.0.0",
47
- "@stigmer/protos": "3.2.3"
48
+ "@stigmer/protos": "3.4.0"
48
49
  }
49
50
  }
package/session.d.ts CHANGED
@@ -28,4 +28,41 @@ export declare const PENDING_SUBJECT = "Auto-created session";
28
28
  * `client-apps/cli/internal/cli/session/get.go`.
29
29
  */
30
30
  export declare function resolvedSubject(subject: string | undefined): string | null;
31
+ /**
32
+ * `SessionSpec.metadata` key carrying embedder-supplied session context —
33
+ * standing, per-user context (who the caller is, their experience level,
34
+ * their standing instructions) that the agent runner injects into the
35
+ * system prompt on every turn but the conversation UI never renders
36
+ * (stigmer/stigmer#286).
37
+ *
38
+ * Set it at session creation via `session.create({ metadata })` or the
39
+ * one-call bootstrap's `sessionSpec.metadata`, or use the typed
40
+ * `sessionContext` field on the `@stigmer/react` hooks, which maps onto
41
+ * this key via {@link mergeSessionContext}.
42
+ *
43
+ * Personalization, not authorization: anyone who can create the session
44
+ * can set this value (the same trust level as authoring the first
45
+ * message), so agents treat it as context — never as a credential or a
46
+ * permission grant. It is hidden from the conversation thread, not from
47
+ * the API: `session.get` returns the full spec including this key, so it
48
+ * is not a secrets channel — secrets belong in `runtimeEnv` or
49
+ * Environment resources.
50
+ *
51
+ * Pinned verbatim to `SESSION_CONTEXT_METADATA_KEY` in the runner
52
+ * (`backend/services/runner/src/shared/session-context.ts`), with mirror
53
+ * guard tests on both sides — a drift degrades to the agent simply not
54
+ * receiving the context, never worse.
55
+ */
56
+ export declare const SESSION_CONTEXT_METADATA_KEY = "stigmer.ai/session-context";
57
+ /**
58
+ * Fold a typed session-context value into a session's spec metadata map,
59
+ * under {@link SESSION_CONTEXT_METADATA_KEY}.
60
+ *
61
+ * The single owner of the precedence rule: a non-blank `sessionContext`
62
+ * wins over any value already present under the reserved key in
63
+ * `metadata`. A blank/undefined `sessionContext` leaves `metadata`
64
+ * untouched (returned as-is, possibly `undefined` — callers never send
65
+ * an empty map).
66
+ */
67
+ export declare function mergeSessionContext(metadata: Record<string, string> | undefined, sessionContext: string | undefined): Record<string, string> | undefined;
31
68
  //# sourceMappingURL=session.d.ts.map
package/session.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;;;;;;;GAQG;AACH,eAAO,MAAM,eAAe,yBAAyB,CAAC;AAEtD;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAK1E"}
1
+ {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;;;;;;;GAQG;AACH,eAAO,MAAM,eAAe,yBAAyB,CAAC;AAEtD;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAK1E;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,4BAA4B,+BAA+B,CAAC;AAEzE;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,EAC5C,cAAc,EAAE,MAAM,GAAG,SAAS,GACjC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAMpC"}
package/session.js CHANGED
@@ -33,4 +33,47 @@ export function resolvedSubject(subject) {
33
33
  }
34
34
  return subject;
35
35
  }
36
+ /**
37
+ * `SessionSpec.metadata` key carrying embedder-supplied session context —
38
+ * standing, per-user context (who the caller is, their experience level,
39
+ * their standing instructions) that the agent runner injects into the
40
+ * system prompt on every turn but the conversation UI never renders
41
+ * (stigmer/stigmer#286).
42
+ *
43
+ * Set it at session creation via `session.create({ metadata })` or the
44
+ * one-call bootstrap's `sessionSpec.metadata`, or use the typed
45
+ * `sessionContext` field on the `@stigmer/react` hooks, which maps onto
46
+ * this key via {@link mergeSessionContext}.
47
+ *
48
+ * Personalization, not authorization: anyone who can create the session
49
+ * can set this value (the same trust level as authoring the first
50
+ * message), so agents treat it as context — never as a credential or a
51
+ * permission grant. It is hidden from the conversation thread, not from
52
+ * the API: `session.get` returns the full spec including this key, so it
53
+ * is not a secrets channel — secrets belong in `runtimeEnv` or
54
+ * Environment resources.
55
+ *
56
+ * Pinned verbatim to `SESSION_CONTEXT_METADATA_KEY` in the runner
57
+ * (`backend/services/runner/src/shared/session-context.ts`), with mirror
58
+ * guard tests on both sides — a drift degrades to the agent simply not
59
+ * receiving the context, never worse.
60
+ */
61
+ export const SESSION_CONTEXT_METADATA_KEY = "stigmer.ai/session-context";
62
+ /**
63
+ * Fold a typed session-context value into a session's spec metadata map,
64
+ * under {@link SESSION_CONTEXT_METADATA_KEY}.
65
+ *
66
+ * The single owner of the precedence rule: a non-blank `sessionContext`
67
+ * wins over any value already present under the reserved key in
68
+ * `metadata`. A blank/undefined `sessionContext` leaves `metadata`
69
+ * untouched (returned as-is, possibly `undefined` — callers never send
70
+ * an empty map).
71
+ */
72
+ export function mergeSessionContext(metadata, sessionContext) {
73
+ const context = sessionContext?.trim();
74
+ if (!context) {
75
+ return metadata;
76
+ }
77
+ return { ...metadata, [SESSION_CONTEXT_METADATA_KEY]: context };
78
+ }
36
79
  //# sourceMappingURL=session.js.map
package/session.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"session.js","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,sBAAsB,CAAC;AAEtD;;;;;;;;;GASG;AACH,MAAM,UAAU,eAAe,CAAC,OAA2B;IACzD,IAAI,CAAC,OAAO,IAAI,OAAO,KAAK,eAAe,EAAE,CAAC;QAC5C,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC"}
1
+ {"version":3,"file":"session.js","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,sBAAsB,CAAC;AAEtD;;;;;;;;;GASG;AACH,MAAM,UAAU,eAAe,CAAC,OAA2B;IACzD,IAAI,CAAC,OAAO,IAAI,OAAO,KAAK,eAAe,EAAE,CAAC;QAC5C,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,CAAC,MAAM,4BAA4B,GAAG,4BAA4B,CAAC;AAEzE;;;;;;;;;GASG;AACH,MAAM,UAAU,mBAAmB,CACjC,QAA4C,EAC5C,cAAkC;IAElC,MAAM,OAAO,GAAG,cAAc,EAAE,IAAI,EAAE,CAAC;IACvC,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,OAAO,QAAQ,CAAC;IAClB,CAAC;IACD,OAAO,EAAE,GAAG,QAAQ,EAAE,CAAC,4BAA4B,CAAC,EAAE,OAAO,EAAE,CAAC;AAClE,CAAC"}
@@ -8,6 +8,7 @@ import {
8
8
  isTransientStreamError,
9
9
  getUserMessage,
10
10
  getErrorReason,
11
+ getRecordConstraint,
11
12
  annotateRpcError,
12
13
  getRpcMetadata,
13
14
  type ErrorCategory,
@@ -328,6 +329,63 @@ describe("getErrorReason", () => {
328
329
  });
329
330
  });
330
331
 
332
+ describe("getRecordConstraint", () => {
333
+ const recordViolation = (metadata: Record<string, string>) =>
334
+ new ConnectError("that slot is already booked", Code.AlreadyExists, undefined, [
335
+ {
336
+ desc: ErrorInfoSchema,
337
+ value: {
338
+ domain: "datastore.stigmer.ai",
339
+ reason: "CONSTRAINT_VIOLATION",
340
+ metadata,
341
+ },
342
+ },
343
+ ]);
344
+
345
+ it("extracts the constraint name from a record-RPC violation", () => {
346
+ expect(getRecordConstraint(recordViolation({ constraint: "one_confirmed_per_slot" })))
347
+ .toBe("one_confirmed_per_slot");
348
+ });
349
+
350
+ it("walks the cause chain of an SDK-wrapped StigmerError", () => {
351
+ const connectError = recordViolation({ constraint: "one_confirmed_per_slot" });
352
+ const wrapped = new StigmerError(
353
+ "already-exists",
354
+ connectError.rawMessage,
355
+ Code.AlreadyExists,
356
+ { cause: connectError },
357
+ );
358
+ expect(getRecordConstraint(wrapped)).toBe("one_confirmed_per_slot");
359
+ });
360
+
361
+ it("returns null when the ErrorInfo domain is not the records domain", () => {
362
+ const foreign = new ConnectError("workspace taken", Code.FailedPrecondition, undefined, [
363
+ {
364
+ desc: ErrorInfoSchema,
365
+ value: {
366
+ domain: "stigmer.ai",
367
+ reason: "SLACK_WORKSPACE_ALREADY_CONNECTED",
368
+ metadata: { constraint: "should_not_leak" },
369
+ },
370
+ },
371
+ ]);
372
+ expect(getRecordConstraint(foreign)).toBeNull();
373
+ });
374
+
375
+ it("returns null when the violation carries no constraint metadata", () => {
376
+ expect(getRecordConstraint(recordViolation({}))).toBeNull();
377
+ expect(getRecordConstraint(recordViolation({ constraint: "" }))).toBeNull();
378
+ });
379
+
380
+ it("returns null for errors without ErrorInfo", () => {
381
+ expect(
382
+ getRecordConstraint(new ConnectError("bare", Code.AlreadyExists)),
383
+ ).toBeNull();
384
+ expect(getRecordConstraint(new Error("plain"))).toBeNull();
385
+ expect(getRecordConstraint(null)).toBeNull();
386
+ });
387
+ });
388
+
331
389
  describe("annotateRpcError / getRpcMetadata", () => {
332
390
  it("roundtrips metadata on an error object", () => {
333
391
  const err = new Error("test");
@@ -0,0 +1,271 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { create, equals } from "@bufbuild/protobuf";
3
+ import { AgentSchema, type Agent } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
4
+ import { EnvironmentSchema, type Environment } from "@stigmer/protos/ai/stigmer/agentic/environment/v1/api_pb";
5
+ import { ApiResourceMetadataSchema } from "@stigmer/protos/ai/stigmer/commons/apiresource/metadata_pb";
6
+ import { parse as parseYaml } from "yaml";
7
+ import { parseManifest } from "../manifest/parse";
8
+ import { serializeManifest } from "../manifest/serialize";
9
+ import { manifestKinds, manifestHandlerForYamlKind } from "../manifest/registry";
10
+
11
+ /**
12
+ * Manifest engine contract tests.
13
+ *
14
+ * The engine's core promise is *round-trip fidelity against the generated
15
+ * proto schemas*: any resource the server returns serializes to YAML that
16
+ * parses back to the same proto (minus system-managed state), and any
17
+ * repo-authored manifest (snake_case fields, `apiVersion` spelling) parses
18
+ * to a proto the `apply` RPC accepts. Fixtures mirror the real manifests
19
+ * in the whatsapp-doctor-assistant dogfooding repo.
20
+ */
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // Fixtures — shaped like the whatsapp-doctor-assistant repo manifests
24
+ // ---------------------------------------------------------------------------
25
+
26
+ const AGENT_YAML = `
27
+ apiVersion: agentic.stigmer.ai/v1
28
+ kind: Agent
29
+ metadata:
30
+ name: clinic-patient-assistant
31
+ org: rakeshreddi098
32
+ spec:
33
+ description: Patient-facing WhatsApp assistant for the clinic.
34
+ instructions: |
35
+ You are the appointment assistant for the clinic.
36
+ Short messages. One question at a time.
37
+ `;
38
+
39
+ const ENVIRONMENT_YAML = `
40
+ apiVersion: agentic.stigmer.ai/v1
41
+ kind: Environment
42
+ metadata:
43
+ name: clinic-patient-db
44
+ org: rakeshreddi098
45
+ spec:
46
+ description: "Clinic records access for the patient assistant"
47
+ data:
48
+ POSTGRES_CONNECTION_URL:
49
+ value: "postgresql://patient_role:secret@host:5432/postgres"
50
+ is_secret: true
51
+ description: "Supabase Postgres connection URL for patient_role"
52
+ `;
53
+
54
+ const AGENT_CHANNEL_YAML = `
55
+ apiVersion: agentic.stigmer.ai/v1
56
+ kind: AgentChannel
57
+ metadata:
58
+ name: clinic-patient-whatsapp
59
+ org: rakeshreddi098
60
+ spec:
61
+ agent_ref:
62
+ kind: agent
63
+ org: rakeshreddi098
64
+ slug: clinic-patient-assistant
65
+ enabled: true
66
+ whatsapp:
67
+ phone_number_id: "1234567890"
68
+ app_ref:
69
+ kind: channel_app
70
+ org: rakeshreddi098
71
+ slug: hosipital
72
+ environment_refs:
73
+ - kind: environment
74
+ org: rakeshreddi098
75
+ slug: clinic-patient-db
76
+ `;
77
+
78
+ // ---------------------------------------------------------------------------
79
+ // parseManifest
80
+ // ---------------------------------------------------------------------------
81
+
82
+ describe("parseManifest", () => {
83
+ it("parses a snake_case Agent manifest into a full Agent proto", () => {
84
+ const docs = parseManifest(AGENT_YAML);
85
+ expect(docs).toHaveLength(1);
86
+
87
+ const doc = docs[0];
88
+ expect(doc.handler.yamlKind).toBe("Agent");
89
+ expect(doc.name).toBe("clinic-patient-assistant");
90
+ expect(doc.org).toBe("rakeshreddi098");
91
+
92
+ const agent = doc.message as Agent;
93
+ expect(agent.spec?.description).toBe(
94
+ "Patient-facing WhatsApp assistant for the clinic.",
95
+ );
96
+ expect(agent.spec?.instructions).toContain("appointment assistant");
97
+ });
98
+
99
+ it("parses an AgentChannel manifest with enum-valued resource refs", () => {
100
+ const docs = parseManifest(AGENT_CHANNEL_YAML);
101
+ expect(docs).toHaveLength(1);
102
+ expect(docs[0].handler.yamlKind).toBe("AgentChannel");
103
+ expect(docs[0].slug).toBe("clinic-patient-whatsapp");
104
+ });
105
+
106
+ it("sorts multi-document manifests into dependency apply order", () => {
107
+ // Authored channel-first; the Environment must still apply first.
108
+ const docs = parseManifest(`${AGENT_CHANNEL_YAML}\n---\n${ENVIRONMENT_YAML}`);
109
+ expect(docs.map((d) => d.handler.yamlKind)).toEqual([
110
+ "Environment",
111
+ "AgentChannel",
112
+ ]);
113
+ });
114
+
115
+ it("injects the target org when the document omits it", () => {
116
+ const yaml = AGENT_YAML.replace(" org: rakeshreddi098\n", "");
117
+ const docs = parseManifest(yaml, { org: "acme" });
118
+ expect(docs[0].org).toBe("acme");
119
+ expect(docs[0].warning).toBeUndefined();
120
+ });
121
+
122
+ it("honors the document org and warns when it differs from the target", () => {
123
+ const docs = parseManifest(AGENT_YAML, { org: "acme" });
124
+ expect(docs[0].org).toBe("rakeshreddi098");
125
+ expect(docs[0].warning).toContain('"rakeshreddi098"');
126
+ expect(docs[0].warning).toContain('"acme"');
127
+ });
128
+
129
+ it("rejects unknown fields loudly (strict schema contract)", () => {
130
+ const yaml = AGENT_YAML.replace("spec:", "spec:\n instrctions: typo");
131
+ expect(() => parseManifest(yaml)).toThrow(/Invalid Agent/);
132
+ });
133
+
134
+ it("rejects a document without a kind", () => {
135
+ expect(() => parseManifest("metadata:\n name: x\n")).toThrow(/'kind'/);
136
+ });
137
+
138
+ it("rejects unsupported kinds with the supported list", () => {
139
+ expect(() => parseManifest("kind: Skill\nmetadata:\n name: x\n")).toThrow(
140
+ /Unsupported resource kind "Skill".*Agent/s,
141
+ );
142
+ });
143
+
144
+ it("rejects empty input with actionable guidance", () => {
145
+ expect(() => parseManifest(" \n")).toThrow(/empty/i);
146
+ });
147
+
148
+ it("rejects malformed YAML", () => {
149
+ expect(() => parseManifest("kind: Agent\n bad indent: [")).toThrow(
150
+ /Invalid YAML/,
151
+ );
152
+ });
153
+ });
154
+
155
+ // ---------------------------------------------------------------------------
156
+ // serializeManifest
157
+ // ---------------------------------------------------------------------------
158
+
159
+ describe("serializeManifest", () => {
160
+ const agent = create(AgentSchema, {
161
+ apiVersion: "agentic.stigmer.ai/v1",
162
+ kind: "Agent",
163
+ metadata: {
164
+ id: "agt_01example",
165
+ name: "clinic-patient-assistant",
166
+ slug: "clinic-patient-assistant",
167
+ org: "rakeshreddi098",
168
+ version: { id: "ver_01", message: "server-managed" },
169
+ },
170
+ spec: {
171
+ description: "Patient-facing WhatsApp assistant.",
172
+ instructions: "Short messages.\nOne question at a time.\n",
173
+ },
174
+ status: { defaultInstanceId: "agi_01example" },
175
+ });
176
+
177
+ it("emits the canonical envelope: apiVersion spelling, no status", () => {
178
+ const yaml = serializeManifest(agent);
179
+ const doc = parseYaml(yaml) as Record<string, unknown>;
180
+
181
+ expect(Object.keys(doc)[0]).toBe("apiVersion");
182
+ expect(doc.apiVersion).toBe("agentic.stigmer.ai/v1");
183
+ expect(doc.kind).toBe("Agent");
184
+ expect(doc.status).toBeUndefined();
185
+ expect(yaml).not.toContain("api_version:");
186
+ });
187
+
188
+ it("keeps metadata.id but strips server-managed metadata.version", () => {
189
+ const doc = parseYaml(serializeManifest(agent)) as {
190
+ metadata: Record<string, unknown>;
191
+ };
192
+ expect(doc.metadata.id).toBe("agt_01example");
193
+ expect(doc.metadata.version).toBeUndefined();
194
+ });
195
+
196
+ it("fills apiVersion and kind from the registry when the proto omits them", () => {
197
+ const bare = create(AgentSchema, {
198
+ metadata: { name: "x", org: "acme" },
199
+ });
200
+ const doc = parseYaml(serializeManifest(bare)) as Record<string, unknown>;
201
+ expect(doc.apiVersion).toBe("agentic.stigmer.ai/v1");
202
+ expect(doc.kind).toBe("Agent");
203
+ });
204
+
205
+ it("round-trips through parseManifest to an equal proto", () => {
206
+ const docs = parseManifest(serializeManifest(agent));
207
+ expect(docs).toHaveLength(1);
208
+
209
+ // The round-tripped proto matches the original minus system-managed
210
+ // state (status, metadata.version) — exactly what apply consumes.
211
+ const expected = create(AgentSchema, {
212
+ ...agent,
213
+ status: undefined,
214
+ metadata: { ...agent.metadata!, version: undefined },
215
+ });
216
+ expect(equals(AgentSchema, docs[0].message as Agent, expected)).toBe(true);
217
+ });
218
+
219
+ it("round-trips Environment secret values byte-identically", () => {
220
+ const env = create(EnvironmentSchema, {
221
+ metadata: { name: "clinic-patient-db", org: "rakeshreddi098" },
222
+ spec: {
223
+ data: {
224
+ POSTGRES_CONNECTION_URL: {
225
+ value: "***REDACTED***",
226
+ isSecret: true,
227
+ description: "Supabase Postgres connection URL",
228
+ },
229
+ },
230
+ },
231
+ });
232
+
233
+ const docs = parseManifest(serializeManifest(env));
234
+ const roundTripped = docs[0].message as Environment;
235
+ expect(roundTripped.spec?.data.POSTGRES_CONNECTION_URL?.value).toBe(
236
+ "***REDACTED***",
237
+ );
238
+ expect(roundTripped.spec?.data.POSTGRES_CONNECTION_URL?.isSecret).toBe(true);
239
+ });
240
+
241
+ it("rejects messages of kinds outside the registry", () => {
242
+ // ApiResourceMetadata is a real proto message but not a manifest kind.
243
+ const notAResource = create(ApiResourceMetadataSchema, { name: "x" });
244
+ expect(() => serializeManifest(notAResource)).toThrow(/not.*registry/i);
245
+ });
246
+ });
247
+
248
+ // ---------------------------------------------------------------------------
249
+ // Registry invariants
250
+ // ---------------------------------------------------------------------------
251
+
252
+ describe("manifest registry", () => {
253
+ it("every handler resolves by its own YAML kind", () => {
254
+ for (const handler of manifestKinds()) {
255
+ expect(manifestHandlerForYamlKind(handler.yamlKind)).toBe(handler);
256
+ }
257
+ });
258
+
259
+ it("apply order is strictly increasing (no accidental ties)", () => {
260
+ const orders = manifestKinds().map((h) => h.applyOrder);
261
+ for (let i = 1; i < orders.length; i++) {
262
+ expect(orders[i]).toBeGreaterThan(orders[i - 1]);
263
+ }
264
+ });
265
+
266
+ it("covers the kinds the console flows depend on", () => {
267
+ for (const kind of ["Agent", "McpServer", "Workflow", "Environment", "AgentChannel", "ChannelApp"]) {
268
+ expect(manifestHandlerForYamlKind(kind), `missing handler for ${kind}`).toBeDefined();
269
+ }
270
+ });
271
+ });
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Unit tests for the hand-written session utilities: the pinned
3
+ * session-context metadata key and the typed-context merge semantics.
4
+ */
5
+
6
+ import { describe, it, expect } from "vitest";
7
+ import {
8
+ SESSION_CONTEXT_METADATA_KEY,
9
+ mergeSessionContext,
10
+ } from "../session.js";
11
+
12
+ describe("SESSION_CONTEXT_METADATA_KEY", () => {
13
+ it("is pinned verbatim to the runner's constant (mirror guard)", () => {
14
+ // Pinned to SESSION_CONTEXT_METADATA_KEY in
15
+ // backend/services/runner/src/shared/session-context.ts. Changing
16
+ // either side alone silently blinds the agent to the embedder's
17
+ // context; change BOTH together.
18
+ expect(SESSION_CONTEXT_METADATA_KEY).toBe("stigmer.ai/session-context");
19
+ });
20
+ });
21
+
22
+ describe("mergeSessionContext", () => {
23
+ it("folds the context into the metadata map under the reserved key", () => {
24
+ expect(mergeSessionContext({ "acme/tenant": "t-1" }, "Role: admin")).toEqual({
25
+ "acme/tenant": "t-1",
26
+ [SESSION_CONTEXT_METADATA_KEY]: "Role: admin",
27
+ });
28
+ });
29
+
30
+ it("creates the map when metadata is undefined", () => {
31
+ expect(mergeSessionContext(undefined, "Role: admin")).toEqual({
32
+ [SESSION_CONTEXT_METADATA_KEY]: "Role: admin",
33
+ });
34
+ });
35
+
36
+ it("lets the typed field win over a raw entry under the reserved key", () => {
37
+ expect(
38
+ mergeSessionContext(
39
+ { [SESSION_CONTEXT_METADATA_KEY]: "stale raw value" },
40
+ "typed value",
41
+ ),
42
+ ).toEqual({ [SESSION_CONTEXT_METADATA_KEY]: "typed value" });
43
+ });
44
+
45
+ it("returns metadata untouched when the context is undefined", () => {
46
+ const metadata = { "acme/tenant": "t-1" };
47
+ expect(mergeSessionContext(metadata, undefined)).toBe(metadata);
48
+ });
49
+
50
+ it("returns undefined when both inputs are absent — callers never send an empty map", () => {
51
+ expect(mergeSessionContext(undefined, undefined)).toBeUndefined();
52
+ expect(mergeSessionContext(undefined, " ")).toBeUndefined();
53
+ });
54
+
55
+ it("trims the context before storing (blank means absent)", () => {
56
+ expect(mergeSessionContext(undefined, " Role: admin ")).toEqual({
57
+ [SESSION_CONTEXT_METADATA_KEY]: "Role: admin",
58
+ });
59
+ });
60
+
61
+ it("does not mutate the caller's metadata map", () => {
62
+ const metadata = { "acme/tenant": "t-1" };
63
+ mergeSessionContext(metadata, "Role: admin");
64
+ expect(metadata).toEqual({ "acme/tenant": "t-1" });
65
+ });
66
+ });