@loomcycle/client 1.112.0 → 1.113.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -14,7 +14,7 @@
14
14
  * getAgent(agentId): Promise<Agent>
15
15
  * getRun(runId): Promise<Agent>
16
16
  * cancelAgent(agentId, opts?): Promise<CancelAgentResult>
17
- * listUserAgents(userId, opts?): Promise<Agent[]>
17
+ * listUserAgents(userId, opts?): Promise<AgentList>
18
18
  * listWalkRuns(walkId, opts?): Promise<WalkRunsPage>
19
19
  * deleteRunTree(runId, opts?): Promise<RunTreeDeletion> // needs runs:delete
20
20
  * deleteSession(sessionId, {cascade: "children"}): Promise<RunTreeDeletion>
@@ -35,6 +35,9 @@
35
35
  * resumeRuntime(): Promise<ResumeResult>
36
36
  * getRuntimeState(): Promise<RuntimeStateResponse>
37
37
  * resolveProbe(opts?): Promise<ResolverMatrix>
38
+ * listModels(): Promise<ModelAliasesResponse> // configured model aliases
39
+ * getRouting(): Promise<RoutingResponse> // per user_tier × tier cascade
40
+ * listProviderModels(providerId): Promise<ProviderModelsResponse> // admin token only
38
41
  *
39
42
  * // Snapshot lifecycle (v0.8.17/8.18)
40
43
  * createSnapshot(opts?): Promise<SnapshotCreateResponse>
@@ -57,6 +60,10 @@
57
60
  * backfillEmbeddings(scope, scopeId, opts?): Promise<MemoryBackfillResponse> // embeds rows that have none
58
61
  * purgeStaleEmbeddings(scope, scopeId, opts?): Promise<MemoryPurgeResponse> // DELETES; dry_run defaults true
59
62
  *
63
+ * // Memory / document change feeds (RFC CD Part C; SSE; substrate:tenant)
64
+ * streamMemoryChanges(opts?): AsyncIterable<ChangeFeedFrame> // feed | change | error (non-fatal); resume with opts.since = last seq
65
+ * streamDocumentChanges(opts?): AsyncIterable<ChangeFeedFrame>
66
+ *
60
67
  * // Interruption (v0.8.16; decline RFC BH)
61
68
  * listUserInterrupts(userId, opts?): Promise<InterruptListResponse>
62
69
  * listRunInterrupts(runId, opts?): Promise<InterruptListResponse>
@@ -72,7 +79,27 @@
72
79
  * mcpServerDef(input): Promise<SubstrateToolResponse>
73
80
  * mcpServerDefVerify(name, sha): Promise<MCPServerDefVerifyResult> // v0.18.0
74
81
  * ensureMcpServer(opts): Promise<EnsureMcpServerResult> // v0.18.0 — idempotent register-if-changed
75
- * scheduleDef(input): Promise<SubstrateToolResponse>
82
+ * scheduleDef(input): Promise<SubstrateToolResponse> // input: ScheduleDefInput | SubstrateToolInput
83
+ * webhookDef(input): Promise<SubstrateToolResponse> // input: WebhookDefInput | SubstrateToolInput
84
+ * hookDef(input): Promise<SubstrateToolResponse> // input: HookDefInput | SubstrateToolInput
85
+ * ensureHookDef(name, overlay, opts?): Promise<EnsureHookDefResult> // idempotent register-if-changed
86
+ * createHookDef / forkHookDef(name, overlay, opts?) // overlay strictly typed, no loose fallback
87
+ * createScheduleDef / forkScheduleDef(name, overlay, opts?) // ditto (overlay open: types of known keys checked)
88
+ * createWebhookDef / forkWebhookDef(name, overlay, opts?) // ditto
89
+ *
90
+ * // Def name enumeration (tenant-scoped; opts.tenant = admin focus)
91
+ * listAgentDefNames / listSkillDefNames / listHookDefNames / listMcpServerDefNames /
92
+ * listScheduleDefNames / listA2AServerCardDefNames / listA2AAgentDefNames /
93
+ * listWebhookDefNames / listMemoryBackendDefNames / listDocumentSourceDefNames
94
+ * (opts?): Promise<DefNamesResponse>
95
+ * listOperatorTokenDefNames(opts?): Promise<OperatorTokenDefNamesResponse> // admin token only
96
+ *
97
+ * // Schedules admin
98
+ * listAllSchedules(opts?): Promise<ScheduleListResponse> // static yaml + ScheduleDefs, merged
99
+ * getScheduleState(defId): Promise<ScheduleStateView>
100
+ * runScheduleNow(defId): Promise<ScheduleRunNowResult> // fires on the next tick
101
+ * pauseSchedule(defId): Promise<SchedulePauseResult> // indefinite
102
+ * resumeSchedule(defId): Promise<SchedulePauseResult>
76
103
  *
77
104
  * // Dynamic filesystem volumes (v0.35.0 — RFC AH; tenant-confined)
78
105
  * volumeDef(input): Promise<SubstrateToolResponse> // create/get/list/delete/purge
@@ -83,8 +110,8 @@
83
110
  * listTeams(): Promise<ListTeamsResponse>
84
111
  * renderTeamDiagram(name, opts?): Promise<TeamDiagram>
85
112
  * getTeamDef(defId): Promise<TeamDefDetail>
86
- * createTeam(name, overlay): Promise<CreatedTeam>
87
- * forkTeam(name, overlay): Promise<CreatedTeam>
113
+ * createTeam(name, overlay, opts?): Promise<CreatedTeam> // opts: promote (default true) / description
114
+ * forkTeam(name, overlay, opts?): Promise<CreatedTeam> // opts: promote / parentDefId / description
88
115
  * deleteTeam(name): Promise<{name, deleted}>
89
116
  * runTeam({name|defId, input}): Promise<TeamRunResult>
90
117
  * runTeam({..., mode:"detach"}): Promise<TeamRunDetached> // the handle, now — the walk runs on
@@ -98,6 +125,14 @@
98
125
  * // History — browse/search/annotate past chats (RFC BE; tenant-confined)
99
126
  * history(input): Promise<HistoryToolResponse> // list/get/search/rename/annotate/pin/archive/recap/resume
100
127
  *
128
+ * // Evaluation — score runs and read the scores back, outside a run
129
+ * evaluation(input): Promise<…> // submit/get/list_for_run/list_for_def/aggregate
130
+ * submitEvaluation(runId, score, opts?): Promise<EvaluationSubmitResult>
131
+ * getEvaluation(evalId): Promise<EvaluationRecord>
132
+ * listEvaluationsForRun(runId, opts?): Promise<EvaluationListForRunResult>
133
+ * listEvaluationsForDef(defId, opts?): Promise<EvaluationListForDefResult>
134
+ * aggregateEvaluations(defId, opts?): Promise<EvaluationAggregate>
135
+ *
101
136
  * // Library v2 enumeration (v0.10.3 — yaml+substrate merged)
102
137
  * listLibraryAgents(): Promise<LibraryListResponse<LibraryAgentDefinition>>
103
138
  * listLibrarySkills(): Promise<LibraryListResponse<LibrarySkillDefinition>>
@@ -114,13 +149,18 @@
114
149
  * full HTTP-status → typed-error mapping table):
115
150
  * LoomcycleError, AgentNotFoundError, SessionNotFoundError,
116
151
  * SessionBusyError, AgentIDInUseError, BackpressureError,
117
- * AuthError, UnavailableError, InvalidArgumentError,
152
+ * RateLimitedError (subclass of BackpressureError),
153
+ * PerUserQuotaExhaustedError, AuthError, InsufficientScopeError (a 403
154
+ * naming the scope the token lacks), UnavailableError,
155
+ * InvalidArgumentError,
118
156
  * PauseNotConfiguredError (subclass of UnavailableError),
119
157
  * AlreadyPausingError, NotPausedError, SnapshotNotFoundError,
120
158
  * SnapshotTooLargeError, RequestTooLargeError + PromptTooLargeError
121
159
  * (both subclasses of SnapshotTooLargeError), TokenLimitExceededError
122
- * (subclass of BackpressureError), SnapshotVersionError,
123
- * SubstrateToolRefusedError (v0.8.22)
160
+ * (NOT a BackpressureError: a hard budget, not retryable),
161
+ * SnapshotVersionError, SubstrateToolRefusedError (v0.8.22).
162
+ * Every one carries `retryable` (true only for a refusal that clears
163
+ * on its own).
124
164
  *
125
165
  * Transport: HTTP+SSE. Auth: Bearer token via the Authorization
126
166
  * header. Designed for Node ≥18 (engines pinned); Bun/Deno likely
@@ -131,5 +171,6 @@
131
171
  */
132
172
  export { LoomcycleClient } from "./client.js";
133
173
  export { InteractiveSession } from "./interactive.js";
174
+ export { verifyLoomcycleSignature, verifyLoomcycleDelivery } from "./signature.js";
134
175
  export { ClientToolHost, clientToolsURL, CLIENT_TOOL_SUBPROTOCOL } from "./client-tools.js";
135
- export { AgentIDInUseError, AgentNotFoundError, AlreadyPausingError, AuthError, BackpressureError, NotFoundError, InvalidArgumentError, ChannelCursorRegressionError, LoomcycleError, NotPausedError, PauseNotConfiguredError, PerUserQuotaExhaustedError, PromptTooLargeError, RequestTooLargeError, SessionBusyError, SessionNotFoundError, SnapshotNotFoundError, SnapshotTooLargeError, SnapshotVersionError, SubstrateToolRefusedError, TokenLimitExceededError, UnavailableError, } from "./errors.js";
176
+ export { AgentIDInUseError, AgentNotFoundError, AlreadyPausingError, AuthError, BackpressureError, NotFoundError, InsufficientScopeError, InvalidArgumentError, ChannelCursorRegressionError, LoomcycleError, NotPausedError, PauseNotConfiguredError, PerUserQuotaExhaustedError, PromptTooLargeError, RateLimitedError, RequestTooLargeError, SessionBusyError, SessionNotFoundError, SnapshotNotFoundError, SnapshotTooLargeError, SnapshotVersionError, SubstrateToolRefusedError, TokenLimitExceededError, UnavailableError, } from "./errors.js";
@@ -46,7 +46,11 @@ export class InteractiveSession {
46
46
  else if (ev.type === "session" && ev.session_id) {
47
47
  this.sessionId = ev.session_id;
48
48
  }
49
- this.awaitingInput = ev.type === "awaiting_input";
49
+ // A queued/admitted frame reports the run's admission, not activity in
50
+ // its conversation, so it leaves awaitingInput as it was.
51
+ if (ev.type !== "queued" && ev.type !== "admitted") {
52
+ this.awaitingInput = ev.type === "awaiting_input";
53
+ }
50
54
  yield ev;
51
55
  }
52
56
  }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Verifying a signed loomcycle hook delivery.
3
+ *
4
+ * A HookDef whose webhook body names a `signing_secret` signs every call it
5
+ * makes. The call carries
6
+ *
7
+ * X-Loomcycle-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256>
8
+ *
9
+ * where the MAC is keyed by the secret and computed over `<t>.<raw body>` —
10
+ * the exact bytes of the request body. Only the body is signed: the body's
11
+ * `delivery_id` (a retry keeps it) is what a receiver dedupes on, and its
12
+ * `hook_name` / `owner` say which hook sent it. `X-Loomcycle-Delivery` and
13
+ * `X-Loomcycle-Hook` are UNSIGNED copies of those two fields — a replayer can
14
+ * change them — so never dedupe or authorize on them.
15
+ * {@link verifyLoomcycleDelivery} verifies and returns the body's identity.
16
+ *
17
+ * Web Crypto only (`crypto.subtle`), so the same function runs in Node and in
18
+ * a browser or edge runtime. It is async because Web Crypto is.
19
+ */
20
+ /** Options for {@link verifyLoomcycleSignature}. */
21
+ export interface VerifySignatureOptions {
22
+ /** How far `t` may be from now, either way, in seconds. Default 300 — the
23
+ * window loomcycle's own receiver uses. A larger window lets a captured
24
+ * delivery be replayed for longer. */
25
+ toleranceSec?: number;
26
+ /** The current time in milliseconds since the epoch, as `Date.now()`
27
+ * returns. For tests; defaults to `Date.now()`. */
28
+ now?: number;
29
+ /** The Web Crypto implementation to use. Defaults to
30
+ * `globalThis.crypto.subtle`, which Node 19+ and every browser have; on
31
+ * Node 18 pass `require("node:crypto").webcrypto.subtle`. */
32
+ subtle?: SubtleCrypto;
33
+ }
34
+ /**
35
+ * Reports whether `header` (the `X-Loomcycle-Signature` value) is a valid
36
+ * signature of `rawBody` under `secret`, made within the tolerance of now.
37
+ *
38
+ * `rawBody` must be the request body exactly as received — the string or the
39
+ * bytes, before any JSON parsing. A body parsed and re-serialized will not
40
+ * match: key order, spacing and escapes all change the bytes. A string is
41
+ * hashed as its UTF-8 encoding.
42
+ *
43
+ * Fails closed: a missing or repeated-garbage header, a `t` that is not an
44
+ * integer, a stale or future `t`, a `v1` that is not hex, or a wrong MAC all
45
+ * return `false`, with no reason given. The MAC is compared by Web Crypto's
46
+ * HMAC verify, which compares in constant time. Throws only when no Web Crypto
47
+ * implementation is available (a configuration problem, not a bad request).
48
+ */
49
+ export declare function verifyLoomcycleSignature(rawBody: string | Uint8Array, header: string, secret: string, opts?: VerifySignatureOptions): Promise<boolean>;
50
+ /** A delivery {@link verifyLoomcycleDelivery} verified: what to dedupe on and
51
+ * which hook sent it, read from the signed body, plus the parsed body. */
52
+ export interface VerifiedDelivery<T = Record<string, unknown>> {
53
+ /** The body's `delivery_id`. Dedupe on this — keep each id at least as long
54
+ * as the tolerance window — never on the `X-Loomcycle-Delivery` header,
55
+ * which is an unsigned copy a replayer can change. */
56
+ deliveryId: string;
57
+ /** The body's `hook_name`: the HookDef that sent it. The `X-Loomcycle-Hook`
58
+ * header is an unsigned copy. */
59
+ hookName: string;
60
+ /** The body's `owner` (`agent:<name>`, `team:<name>`, `channel:<name>`). */
61
+ owner: string;
62
+ /** The parsed body. */
63
+ body: T;
64
+ }
65
+ /**
66
+ * {@link verifyLoomcycleSignature}, then parse the verified body and return
67
+ * the identity a receiver should act on: its `delivery_id`, `hook_name` and
68
+ * `owner`. Resolves `null` when the signature does not verify, or the body is
69
+ * not a JSON object carrying a non-empty string `delivery_id` and `hook_name`
70
+ * — a signed delivery always has both. Deduplicating on `deliveryId` stays the
71
+ * caller's job.
72
+ */
73
+ export declare function verifyLoomcycleDelivery<T = Record<string, unknown>>(rawBody: string | Uint8Array, header: string, secret: string, opts?: VerifySignatureOptions): Promise<VerifiedDelivery<T> | null>;
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Verifying a signed loomcycle hook delivery.
3
+ *
4
+ * A HookDef whose webhook body names a `signing_secret` signs every call it
5
+ * makes. The call carries
6
+ *
7
+ * X-Loomcycle-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256>
8
+ *
9
+ * where the MAC is keyed by the secret and computed over `<t>.<raw body>` —
10
+ * the exact bytes of the request body. Only the body is signed: the body's
11
+ * `delivery_id` (a retry keeps it) is what a receiver dedupes on, and its
12
+ * `hook_name` / `owner` say which hook sent it. `X-Loomcycle-Delivery` and
13
+ * `X-Loomcycle-Hook` are UNSIGNED copies of those two fields — a replayer can
14
+ * change them — so never dedupe or authorize on them.
15
+ * {@link verifyLoomcycleDelivery} verifies and returns the body's identity.
16
+ *
17
+ * Web Crypto only (`crypto.subtle`), so the same function runs in Node and in
18
+ * a browser or edge runtime. It is async because Web Crypto is.
19
+ */
20
+ const DEFAULT_TOLERANCE_SEC = 300;
21
+ const HEX_RE = /^(?:[0-9a-fA-F]{2})+$/;
22
+ // What Go's strconv.ParseInt accepts, so a header loomcycle's own receiver
23
+ // would accept is accepted here too.
24
+ const UNIX_RE = /^[+-]?\d+$/;
25
+ /**
26
+ * Reports whether `header` (the `X-Loomcycle-Signature` value) is a valid
27
+ * signature of `rawBody` under `secret`, made within the tolerance of now.
28
+ *
29
+ * `rawBody` must be the request body exactly as received — the string or the
30
+ * bytes, before any JSON parsing. A body parsed and re-serialized will not
31
+ * match: key order, spacing and escapes all change the bytes. A string is
32
+ * hashed as its UTF-8 encoding.
33
+ *
34
+ * Fails closed: a missing or repeated-garbage header, a `t` that is not an
35
+ * integer, a stale or future `t`, a `v1` that is not hex, or a wrong MAC all
36
+ * return `false`, with no reason given. The MAC is compared by Web Crypto's
37
+ * HMAC verify, which compares in constant time. Throws only when no Web Crypto
38
+ * implementation is available (a configuration problem, not a bad request).
39
+ */
40
+ export async function verifyLoomcycleSignature(rawBody, header, secret, opts = {}) {
41
+ const subtle = opts.subtle ?? globalThis.crypto?.subtle;
42
+ if (!subtle) {
43
+ throw new TypeError("verifyLoomcycleSignature: no Web Crypto implementation; pass opts.subtle (on Node 18: require('node:crypto').webcrypto.subtle)");
44
+ }
45
+ if (typeof header !== "string" || typeof secret !== "string" || secret === "") {
46
+ return false;
47
+ }
48
+ let ts = "";
49
+ let macHex = "";
50
+ for (const raw of header.split(",")) {
51
+ const part = raw.trim();
52
+ if (part.startsWith("t="))
53
+ ts = part.slice(2);
54
+ else if (part.startsWith("v1="))
55
+ macHex = part.slice(3);
56
+ }
57
+ if (!UNIX_RE.test(ts) || !HEX_RE.test(macHex)) {
58
+ return false;
59
+ }
60
+ const t = Number(ts);
61
+ if (!Number.isSafeInteger(t)) {
62
+ return false;
63
+ }
64
+ const tolerance = opts.toleranceSec ?? DEFAULT_TOLERANCE_SEC;
65
+ const nowSec = (opts.now ?? Date.now()) / 1000;
66
+ if (!(tolerance >= 0) || Math.abs(nowSec - t) > tolerance) {
67
+ return false;
68
+ }
69
+ const enc = new TextEncoder();
70
+ const body = typeof rawBody === "string" ? enc.encode(rawBody) : rawBody;
71
+ // The MAC covers t exactly as written in the header, then ".", then the body.
72
+ const prefix = enc.encode(ts + ".");
73
+ const signed = new Uint8Array(prefix.length + body.length);
74
+ signed.set(prefix, 0);
75
+ signed.set(body, prefix.length);
76
+ const mac = new Uint8Array(macHex.length / 2);
77
+ for (let i = 0; i < mac.length; i++) {
78
+ mac[i] = parseInt(macHex.slice(i * 2, i * 2 + 2), 16);
79
+ }
80
+ try {
81
+ const key = await subtle.importKey("raw", enc.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["verify"]);
82
+ return await subtle.verify("HMAC", key, mac, signed);
83
+ }
84
+ catch {
85
+ return false;
86
+ }
87
+ }
88
+ /**
89
+ * {@link verifyLoomcycleSignature}, then parse the verified body and return
90
+ * the identity a receiver should act on: its `delivery_id`, `hook_name` and
91
+ * `owner`. Resolves `null` when the signature does not verify, or the body is
92
+ * not a JSON object carrying a non-empty string `delivery_id` and `hook_name`
93
+ * — a signed delivery always has both. Deduplicating on `deliveryId` stays the
94
+ * caller's job.
95
+ */
96
+ export async function verifyLoomcycleDelivery(rawBody, header, secret, opts = {}) {
97
+ if (!(await verifyLoomcycleSignature(rawBody, header, secret, opts))) {
98
+ return null;
99
+ }
100
+ let body;
101
+ try {
102
+ body = JSON.parse(typeof rawBody === "string" ? rawBody : new TextDecoder().decode(rawBody));
103
+ }
104
+ catch {
105
+ return null;
106
+ }
107
+ if (body === null || typeof body !== "object" || Array.isArray(body)) {
108
+ return null;
109
+ }
110
+ const { delivery_id: deliveryId, hook_name: hookName, owner } = body;
111
+ if (typeof deliveryId !== "string" || deliveryId === "" || typeof hookName !== "string" || hookName === "") {
112
+ return null;
113
+ }
114
+ return { deliveryId, hookName, owner: typeof owner === "string" ? owner : "", body: body };
115
+ }