@loomcycle/client 1.121.0 → 1.122.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/client.d.ts CHANGED
@@ -25,7 +25,7 @@
25
25
  */
26
26
  import { InteractiveSession } from "./interactive.js";
27
27
  import { ClientToolHost, type ConnectClientToolsOptions } from "./client-tools.js";
28
- import type { ChangeFeedFrame, StreamChangesOptions, CredentialListResponse, CredentialMeta, CryptoKeyEncoding, CryptoKeyExport, CryptoKeyExternal, CryptoKeyType, CryptoRequest, CryptoResultByOp, DecideRequest, DecideResponse, DecisionModelsResponse, CredentialScope, DirectoryInspection, DirectoryTenant, DirectoryUser, ErasureReport, ErasureResult, Agent, AgentEvent, AgentList, CancelAgentResult, ClientOptions, ContinueOptions, RunOverrideOptions, EffectiveConfigResponse, AgentOverridableResponse, RetuneRunResponse, RunConfigResponse, RunPromptResponse, CreateSnapshotOptions, EnsureCodeAgentOptions, EnsureCodeAgentResult, EnsureHookDefOptions, EnsureHookDefResult, HookDefInput, HookDefOverlay, HookDefRowResponse, CreateDefOptions, ForkDefOptions, HookDefForkOverlay, ScheduleDefInput, ScheduleDefOverlay, ScheduleDefRowResponse, WebhookDefInput, WebhookDefOverlay, WebhookDefRowResponse, EnsureMcpServerOptions, EnsureMcpServerResult, HealthResponse, InterruptListResponse, InterruptStatus, ListUserAgentsOptions, WalkRunsPage, AckChannelOptions, AwaitChannelsOptions, BroadcastChannelsOptions, ChannelAckResult, ChannelAwaitResult, ChannelBroadcastResult, ChannelDescriptor, ChannelPeekResult, ListTeamChannelsOptions, PeekTeamChannelOptions, ReleaseTeamChannelOptions, TeamChannelPeekResult, TeamChannelReleaseResult, TeamChannelsResult, ChannelPublishResult, ChannelPurgeResult, ChannelReleaseResult, ChannelSubscribeResult, CreateChannelOptions, ListChannelsResponse, PeekChannelOptions, PublishChannelOptions, ReleaseChannelOptions, SetMemoryEntryOptions, SetMemoryEntryResponse, SubscribeChannelOptions, UpdateChannelOptions, LibraryAgentDefinition, LibraryListResponse, LibraryMcpServerDefinition, LibrarySkillDefinition, ListUsersResponse, UserRecord, CreateUserBody, UpdateUserBody, MintedUserToken, ListUserTokensResponse, RunnableAgentsResponse, LLMChatOptions, LLMChatResponse, LLMChatStreamItem, LLMEmbeddingsOptions, LLMEmbeddingsResponse, MCPServerDefVerifyResult, MemoryEmbedStatsResponse, MemoryEntriesResponse, MemoryEntryResponse, MemoryBackfillResponse, MemoryPurgeResponse, MemoryReembedResponse, MemoryScopeIDsResponse, MemoryScopesResponse, MemorySearchInput, MemorySearchResponse, PauseResult, RetentionOverride, PersistentVolumesResponse, EphemeralVolumesResponse, ResolveInterruptOptions, ResumeResult, ResolverMatrix, CompactRunResult, RunTreeDeletion, ReplaySessionResult, CancelTurnResult, ReviewRunResult, RunBatchOptions, RunBatchResult, RunOptions, ConfiguredRun, ConfiguredRunPatch, RunStateStreamItem, RuntimeStateResponse, SnapshotCreateResponse, SnapshotDescriptor, SnapshotEnvelope, SnapshotRestoreResponse, StreamUserRunStatesOptions, SubstrateToolInput, SubstrateToolResponse, CreatedTeam, CreateTeamOptions, DefNamesResponse, ForkTeamOptions, ModelAliasesResponse, OperatorTokenDefNamesResponse, ProviderModelsResponse, RoutingResponse, ScheduleListResponse, SchedulePauseResult, ScheduleRunNowResult, ScheduleStateView, ListTeamsResponse, PromotedTeam, RetiredTeam, TeamBreakpoints, TeamDefDetail, TeamRunDetached, TeamRunTarget, TeamVerification, TeamDraft, TeamDraftVerification, TeamVersionList, TeamDiagram, TeamRunResult, PathToolInput, PathToolResponse, DocumentToolInput, DocumentToolResponse, HistoryToolInput, HistoryToolResponse, EvaluationSubmitInput, EvaluationGetInput, EvaluationListForRunInput, EvaluationListForDefInput, EvaluationAggregateInput, EvaluationInput, EvaluationSubmitResult, EvaluationRecord, EvaluationListForRunResult, EvaluationListForDefResult, EvaluationAggregate, TranscriptResponse, WhoamiResponse, UsageDimension, UsageReportResponse, TokenLimit, TokenLimitsResponse, ConfigResponse, SetTokenLimitRequest } from "./types.js";
28
+ import type { ChangeFeedFrame, StreamChangesOptions, CredentialListResponse, CredentialMeta, CryptoKeyEncoding, CryptoKeyExport, CryptoKeyExternal, CryptoKeyType, CryptoRequest, CryptoResultByOp, DecideRequest, DecideResponse, DecisionModelsResponse, CredentialScope, DirectoryInspection, DirectoryTenant, DirectoryUser, ErasureReport, ErasureResult, Agent, AgentEvent, AgentList, CancelAgentResult, ClientOptions, ContinueOptions, RunOverrideOptions, EffectiveConfigResponse, AgentOverridableResponse, RetuneRunResponse, RunConfigResponse, RunPromptResponse, CreateSnapshotOptions, EnsureCodeAgentOptions, EnsureCodeAgentResult, EnsureHookDefOptions, EnsureHookDefResult, HookDefInput, HookDefOverlay, HookDefRowResponse, CreateDefOptions, ForkDefOptions, HookDefForkOverlay, ScheduleDefInput, ScheduleDefOverlay, ScheduleDefRowResponse, WebhookDefInput, WebhookDefOverlay, WebhookDefRowResponse, EnsureMcpServerOptions, EnsureMcpServerResult, HealthResponse, InterruptListResponse, InterruptStatus, ListUserAgentsOptions, WalkRunsPage, AckChannelOptions, AwaitChannelsOptions, BroadcastChannelsOptions, ChannelAckResult, ChannelAwaitResult, ChannelBroadcastResult, ChannelDescriptor, ChannelPeekResult, ListTeamChannelsOptions, PeekTeamChannelOptions, ReleaseTeamChannelOptions, TeamChannelPeekResult, TeamChannelReleaseResult, TeamChannelsResult, ChannelPublishResult, ChannelPurgeResult, ChannelReleaseResult, ChannelSubscribeResult, CreateChannelOptions, ListChannelsResponse, PeekChannelOptions, PublishChannelOptions, ReleaseChannelOptions, SetMemoryEntryOptions, SetMemoryEntryResponse, SubscribeChannelOptions, UpdateChannelOptions, LibraryAgentDefinition, LibraryListResponse, LibraryMcpServerDefinition, LibrarySkillDefinition, ListUsersResponse, UserRecord, CreateUserBody, UpdateUserBody, MintedUserToken, ListUserTokensResponse, RunnableAgentsResponse, LLMChatOptions, LLMChatResponse, LLMChatStreamItem, LLMEmbeddingsOptions, LLMEmbeddingsResponse, MCPServerDefVerifyResult, MemoryEmbedStatsResponse, MemoryEntriesResponse, MemoryEntryResponse, MemoryBackfillResponse, MemoryPurgeResponse, MemoryReembedResponse, MemoryScopeIDsResponse, MemoryScopesResponse, MemorySearchInput, MemorySearchResponse, PauseResult, RetentionOverride, PersistentVolumesResponse, EphemeralVolumesResponse, ResolveInterruptOptions, ResumeResult, ResolverMatrix, CompactRunResult, RunTreeDeletion, ReplaySessionResult, CancelTurnResult, ReviewRunResult, RunBatchOptions, RunBatchResult, RunOptions, ConfiguredRun, ConfiguredRunPatch, RunStateStreamItem, RuntimeStateResponse, SnapshotCreateResponse, SnapshotDescriptor, SnapshotEnvelope, SnapshotRestoreResponse, StreamUserRunStatesOptions, SubstrateToolInput, SubstrateToolResponse, CreatedTeam, CreateTeamOptions, DefNamesResponse, ForkTeamOptions, ModelAliasesResponse, OperatorTokenDefNamesResponse, ProviderModelsResponse, RoutingResponse, ScheduleListResponse, SchedulePauseResult, ScheduleRunNowResult, ScheduleStateView, ListTeamsResponse, PromotedTeam, RetiredTeam, TeamBreakpoints, TeamDefDetail, TeamRunDetached, TeamRunTarget, TeamVerification, TeamDraft, TeamDraftVerification, TeamVersionList, TeamDiagram, TeamRunResult, PathToolInput, PathToolResponse, DocumentToolInput, DocumentToolResponse, HistoryToolInput, HistoryToolResponse, EvaluationSubmitInput, EvaluationGetInput, EvaluationListForRunInput, EvaluationListForDefInput, EvaluationAggregateInput, EvaluationInput, EvaluationSubmitResult, EvaluationRecord, EvaluationListForRunResult, EvaluationListForDefResult, EvaluationAggregate, TranscriptResponse, WhoamiResponse, UsageDimension, UsageReportResponse, TokenLimit, TokenLimitsResponse, ConfigResponse, SetTokenLimitRequest, AclBinding, AclBindingListResponse, AclCatalog, AclDefInput, AclDefListResponse, AclDefNamesResponse, AclDefRow, AclRule, AclValidateResult, AuthzDecision, AuthzEvaluationRequest, AuthzEvaluationsRequest, AuthzEvaluationsResponse, AuthzExplainResponse, AuthzSimulateRequest, AuthzSimulateResponse, CreateAclBindingBody, CreateGroupBody, GroupDetail, GroupEffective, GroupListResponse, GroupMemberChange, GroupMemberKind, GroupRow } from "./types.js";
29
29
  export declare class LoomcycleClient {
30
30
  private ctx;
31
31
  constructor(opts?: ClientOptions);
@@ -1288,14 +1288,184 @@ export declare class LoomcycleClient {
1288
1288
  tenant?: string;
1289
1289
  signal?: AbortSignal;
1290
1290
  }): Promise<DefNamesResponse>;
1291
- /** Every OperatorTokenDef name (`GET /v1/_operatortokendef/names`), with its
1292
- * subject and token counts — never a token. **Admin token only**: token
1291
+ /** Every OperatorTokenDef name (`GET /v1/_operatortokendef/names`), one row
1292
+ * per (tenant, name), with its subject, token counts and current def_id —
1293
+ * never a token. **Admin token only**: token
1293
1294
  * minting stays operator-only, so a `substrate:tenant` token is refused
1294
1295
  * with 403. */
1295
1296
  listOperatorTokenDefNames(opts?: {
1296
1297
  tenant?: string;
1297
1298
  signal?: AbortSignal;
1298
1299
  }): Promise<OperatorTokenDefNamesResponse>;
1300
+ /** `path` with the admin tenant focus and any extra query parameters. */
1301
+ private aclPath;
1302
+ /** Every group of the tenant, stored and config-declared, sorted by name
1303
+ * (`GET /v1/_groups`). */
1304
+ listGroups(opts?: {
1305
+ tenant?: string;
1306
+ signal?: AbortSignal;
1307
+ }): Promise<GroupListResponse>;
1308
+ /** One stored group with its direct members and the groups it is inside of
1309
+ * (`GET /v1/_groups/{name}`). A config-only group is not stored and raises
1310
+ * NotFoundError; {@link LoomcycleClient.getGroupEffective} covers it. */
1311
+ getGroup(name: string, opts?: {
1312
+ tenant?: string;
1313
+ signal?: AbortSignal;
1314
+ }): Promise<GroupDetail>;
1315
+ /** Every user the group holds, through nested groups at any depth and
1316
+ * config-declared members included (`GET /v1/_groups/{name}/effective`). */
1317
+ getGroupEffective(name: string, opts?: {
1318
+ tenant?: string;
1319
+ signal?: AbortSignal;
1320
+ }): Promise<GroupEffective>;
1321
+ /** Create a group, or update its description and `external_id` if the name
1322
+ * exists (`POST /v1/_groups`). A group with an `external_id` is owned by an
1323
+ * identity provider: a later write to it raises a 409 LoomcycleError with
1324
+ * code `group_external`. */
1325
+ createGroup(body: CreateGroupBody, opts?: {
1326
+ tenant?: string;
1327
+ signal?: AbortSignal;
1328
+ }): Promise<GroupRow>;
1329
+ /** Delete a group and every membership edge naming it
1330
+ * (`DELETE /v1/_groups/{name}`). Bindings to it stay and apply to no one.
1331
+ * NotFoundError when there is no such group. */
1332
+ deleteGroup(name: string, opts?: {
1333
+ tenant?: string;
1334
+ signal?: AbortSignal;
1335
+ }): Promise<{
1336
+ deleted: string;
1337
+ }>;
1338
+ /** Add a user or a group to a group (`POST /v1/_groups/{name}/members`). A
1339
+ * group member nests: its members are in this group too. The server
1340
+ * refuses an edge that would close a cycle or nest too deep (400), one
1341
+ * that exists (409 `group_member_exists`), and a delegated writer adding
1342
+ * itself (403 `acl_self_elevation`). */
1343
+ addGroupMember(name: string, member: {
1344
+ kind: GroupMemberKind;
1345
+ id: string;
1346
+ }, opts?: {
1347
+ tenant?: string;
1348
+ signal?: AbortSignal;
1349
+ }): Promise<GroupMemberChange>;
1350
+ /** Remove one membership edge
1351
+ * (`DELETE /v1/_groups/{name}/members/{kind}/{id}`). NotFoundError when
1352
+ * there is no such edge. */
1353
+ removeGroupMember(name: string, kind: GroupMemberKind, id: string, opts?: {
1354
+ tenant?: string;
1355
+ signal?: AbortSignal;
1356
+ }): Promise<GroupMemberChange>;
1357
+ /** The AclDef family, op-dispatched — the same ops the gRPC `AclDef` RPC
1358
+ * and the MCP `acldef` tool take. Every op but `names` posts to
1359
+ * `POST /v1/_acldef`; `names` reads `GET /v1/_acldef/names`, which is
1360
+ * where that op lives on HTTP. The response varies by op; the typed
1361
+ * helpers below return each op's shape. */
1362
+ aclDef(input: AclDefInput, opts?: {
1363
+ tenant?: string;
1364
+ signal?: AbortSignal;
1365
+ }): Promise<unknown>;
1366
+ /** Every rule list of the tenant, stored and config-declared, plus the
1367
+ * built-in baselines (`aclDef` op `list`). Retired lists only with
1368
+ * `includeRetired`. */
1369
+ listAclDefs(opts?: {
1370
+ includeRetired?: boolean;
1371
+ tenant?: string;
1372
+ signal?: AbortSignal;
1373
+ }): Promise<AclDefListResponse>;
1374
+ /** One rule list, retired or not (`aclDef` op `get`). A config-declared
1375
+ * list is returned with `source: "config"`. NotFoundError when absent. */
1376
+ getAclDef(name: string, opts?: {
1377
+ tenant?: string;
1378
+ signal?: AbortSignal;
1379
+ }): Promise<AclDefRow>;
1380
+ /** Create or replace a rule list by name (`aclDef` op `put`): bumps its
1381
+ * version and un-retires it. The rules are validated first; a list the
1382
+ * engine could not enforce is refused with 400. A delegated writer may
1383
+ * not rewrite a list bound to itself (403 `acl_self_elevation`). */
1384
+ putAclDef(body: {
1385
+ name: string;
1386
+ rules: AclRule[];
1387
+ description?: string;
1388
+ }, opts?: {
1389
+ tenant?: string;
1390
+ signal?: AbortSignal;
1391
+ }): Promise<AclDefRow>;
1392
+ /** Retire a rule list (`aclDef` op `retire`). Bindings to it stay and match
1393
+ * nothing; a later `putAclDef` of the name brings it back. NotFoundError
1394
+ * when there is no live list of that name. */
1395
+ retireAclDef(name: string, opts?: {
1396
+ tenant?: string;
1397
+ signal?: AbortSignal;
1398
+ }): Promise<{
1399
+ retired: string;
1400
+ }>;
1401
+ /** Check rules without storing them (`aclDef` op `validate`). Rules the
1402
+ * engine refuses raise InvalidArgumentError (code `acl_invalid`) naming
1403
+ * the fault. */
1404
+ validateAclRules(rules: AclRule[], opts?: {
1405
+ tenant?: string;
1406
+ signal?: AbortSignal;
1407
+ }): Promise<AclValidateResult>;
1408
+ /** The resource types, verbs and transports a rule may name (`aclDef` op
1409
+ * `catalog`). */
1410
+ getAclCatalog(opts?: {
1411
+ tenant?: string;
1412
+ signal?: AbortSignal;
1413
+ }): Promise<AclCatalog>;
1414
+ /** Every live rule-list name a binding in the tenant can reach, stored and
1415
+ * config-declared, sorted (`GET /v1/_acldef/names`). */
1416
+ listAclDefNames(opts?: {
1417
+ tenant?: string;
1418
+ signal?: AbortSignal;
1419
+ }): Promise<AclDefNamesResponse>;
1420
+ /** Every binding of the tenant, stored and config-declared
1421
+ * (`GET /v1/_aclbinding`). Retired ones only with `includeRetired`. */
1422
+ listAclBindings(opts?: {
1423
+ includeRetired?: boolean;
1424
+ tenant?: string;
1425
+ signal?: AbortSignal;
1426
+ }): Promise<AclBindingListResponse>;
1427
+ /** Bind a rule list to a subject (`POST /v1/_aclbinding`). The list must
1428
+ * exist and `expires_at` must be in the future, or InvalidArgumentError
1429
+ * (code `acl_binding_invalid`). A delegated writer may not bind itself, a
1430
+ * group it is in, its token or its tenant (403 `acl_self_elevation`). An
1431
+ * empty `subject_id` / `expires_at` is left out of the request. */
1432
+ createAclBinding(body: CreateAclBindingBody, opts?: {
1433
+ tenant?: string;
1434
+ signal?: AbortSignal;
1435
+ }): Promise<AclBinding>;
1436
+ /** Retire a binding by its `binding_id` (`DELETE /v1/_aclbinding/{id}`).
1437
+ * NotFoundError when there is no live binding of that id. A config
1438
+ * binding has no id and cannot be retired here. */
1439
+ deleteAclBinding(bindingId: string, opts?: {
1440
+ tenant?: string;
1441
+ signal?: AbortSignal;
1442
+ }): Promise<{
1443
+ retired: string;
1444
+ }>;
1445
+ /** May the subject do this? (`POST /v1/_authz/evaluation`, AuthZEN 1.0).
1446
+ * Omit `subject` to ask about yourself. */
1447
+ authzEvaluation(req: AuthzEvaluationRequest, opts?: {
1448
+ tenant?: string;
1449
+ signal?: AbortSignal;
1450
+ }): Promise<AuthzDecision>;
1451
+ /** Up to 256 questions for one subject in one call
1452
+ * (`POST /v1/_authz/evaluations`); decisions come back in order. */
1453
+ authzEvaluations(req: AuthzEvaluationsRequest, opts?: {
1454
+ tenant?: string;
1455
+ signal?: AbortSignal;
1456
+ }): Promise<AuthzEvaluationsResponse>;
1457
+ /** A decision with its working: every rule read for the resource's type
1458
+ * and why it did or did not match (`POST /v1/_authz/explain`). */
1459
+ authzExplain(req: AuthzEvaluationRequest, opts?: {
1460
+ tenant?: string;
1461
+ signal?: AbortSignal;
1462
+ }): Promise<AuthzExplainResponse>;
1463
+ /** What a draft of lists and bindings would decide, beside what is decided
1464
+ * now (`POST /v1/_authz/simulate`). Nothing is written. */
1465
+ authzSimulate(req: AuthzSimulateRequest, opts?: {
1466
+ tenant?: string;
1467
+ signal?: AbortSignal;
1468
+ }): Promise<AuthzSimulateResponse>;
1299
1469
  /** Every schedule: static yaml `scheduled_runs` and ScheduleDefs, merged by
1300
1470
  * name (`GET /v1/_schedules/list-all`), sorted by name. A tenant token sees
1301
1471
  * only its own tenant's ScheduleDefs — static schedules are operator
package/dist/client.js CHANGED
@@ -1496,6 +1496,8 @@ export class LoomcycleClient {
1496
1496
  };
1497
1497
  if (opts.headers)
1498
1498
  overlay.headers = opts.headers;
1499
+ if (opts.binaryResults)
1500
+ overlay.binary_results = opts.binaryResults;
1499
1501
  const createInput = { op: "create", name: opts.name, overlay };
1500
1502
  if (opts.description)
1501
1503
  createInput.description = opts.description;
@@ -1821,13 +1823,183 @@ export class LoomcycleClient {
1821
1823
  async listDocumentSourceDefNames(opts) {
1822
1824
  return this.listDefNames("documentsourcedef", opts);
1823
1825
  }
1824
- /** Every OperatorTokenDef name (`GET /v1/_operatortokendef/names`), with its
1825
- * subject and token counts — never a token. **Admin token only**: token
1826
+ /** Every OperatorTokenDef name (`GET /v1/_operatortokendef/names`), one row
1827
+ * per (tenant, name), with its subject, token counts and current def_id —
1828
+ * never a token. **Admin token only**: token
1826
1829
  * minting stays operator-only, so a `substrate:tenant` token is refused
1827
1830
  * with 403. */
1828
1831
  async listOperatorTokenDefNames(opts) {
1829
1832
  return this.listDefNames("operatortokendef", opts);
1830
1833
  }
1834
+ // ---- Access control: groups, rule lists, bindings, decisions (RFC EY / EZ) ----
1835
+ //
1836
+ // Groups, lists and bindings are the tenant operator's: a `substrate:tenant`
1837
+ // token, an admin, or the legacy shared token. Any other principal is
1838
+ // refused with 403 (InsufficientScopeError). Every call acts in the caller's
1839
+ // own tenant; `tenant` is an admin's focus on another one and is ignored for
1840
+ // a tenant operator. Rows the config declares are listed beside the stored
1841
+ // ones with `source: "config"`; they cannot be written here. Every write
1842
+ // takes effect on every replica at once. A rule that refuses a call throws
1843
+ // AclDeniedError (403 `acl_denied`) naming the rules that decided.
1844
+ //
1845
+ // The decision questions (authzEvaluation / authzEvaluations / authzExplain
1846
+ // / authzSimulate) are open to any principal about itself; naming another
1847
+ // subject of the tenant needs a tenant operator (403 `authz_subject`).
1848
+ // Every call here needs a persistent store (503 otherwise) and is refused
1849
+ // from inside a run.
1850
+ /** `path` with the admin tenant focus and any extra query parameters. */
1851
+ aclPath(path, tenant, extra) {
1852
+ const q = new URLSearchParams(extra);
1853
+ if (tenant)
1854
+ q.set("tenant", tenant);
1855
+ const qs = q.toString();
1856
+ return qs ? `${path}?${qs}` : path;
1857
+ }
1858
+ /** Every group of the tenant, stored and config-declared, sorted by name
1859
+ * (`GET /v1/_groups`). */
1860
+ async listGroups(opts) {
1861
+ return jsonFetch(this.ctx, this.aclPath("/v1/_groups", opts?.tenant), opts);
1862
+ }
1863
+ /** One stored group with its direct members and the groups it is inside of
1864
+ * (`GET /v1/_groups/{name}`). A config-only group is not stored and raises
1865
+ * NotFoundError; {@link LoomcycleClient.getGroupEffective} covers it. */
1866
+ async getGroup(name, opts) {
1867
+ return jsonFetch(this.ctx, this.aclPath(`/v1/_groups/${encodeURIComponent(name)}`, opts?.tenant), opts);
1868
+ }
1869
+ /** Every user the group holds, through nested groups at any depth and
1870
+ * config-declared members included (`GET /v1/_groups/{name}/effective`). */
1871
+ async getGroupEffective(name, opts) {
1872
+ return jsonFetch(this.ctx, this.aclPath(`/v1/_groups/${encodeURIComponent(name)}/effective`, opts?.tenant), opts);
1873
+ }
1874
+ /** Create a group, or update its description and `external_id` if the name
1875
+ * exists (`POST /v1/_groups`). A group with an `external_id` is owned by an
1876
+ * identity provider: a later write to it raises a 409 LoomcycleError with
1877
+ * code `group_external`. */
1878
+ async createGroup(body, opts) {
1879
+ return postJSON(this.ctx, this.aclPath("/v1/_groups", opts?.tenant), body, opts);
1880
+ }
1881
+ /** Delete a group and every membership edge naming it
1882
+ * (`DELETE /v1/_groups/{name}`). Bindings to it stay and apply to no one.
1883
+ * NotFoundError when there is no such group. */
1884
+ async deleteGroup(name, opts) {
1885
+ return deleteJSON(this.ctx, this.aclPath(`/v1/_groups/${encodeURIComponent(name)}`, opts?.tenant), opts);
1886
+ }
1887
+ /** Add a user or a group to a group (`POST /v1/_groups/{name}/members`). A
1888
+ * group member nests: its members are in this group too. The server
1889
+ * refuses an edge that would close a cycle or nest too deep (400), one
1890
+ * that exists (409 `group_member_exists`), and a delegated writer adding
1891
+ * itself (403 `acl_self_elevation`). */
1892
+ async addGroupMember(name, member, opts) {
1893
+ return postJSON(this.ctx, this.aclPath(`/v1/_groups/${encodeURIComponent(name)}/members`, opts?.tenant), { kind: member.kind, id: member.id }, opts);
1894
+ }
1895
+ /** Remove one membership edge
1896
+ * (`DELETE /v1/_groups/{name}/members/{kind}/{id}`). NotFoundError when
1897
+ * there is no such edge. */
1898
+ async removeGroupMember(name, kind, id, opts) {
1899
+ const path = `/v1/_groups/${encodeURIComponent(name)}/members/${encodeURIComponent(kind)}/${encodeURIComponent(id)}`;
1900
+ return deleteJSON(this.ctx, this.aclPath(path, opts?.tenant), opts);
1901
+ }
1902
+ /** The AclDef family, op-dispatched — the same ops the gRPC `AclDef` RPC
1903
+ * and the MCP `acldef` tool take. Every op but `names` posts to
1904
+ * `POST /v1/_acldef`; `names` reads `GET /v1/_acldef/names`, which is
1905
+ * where that op lives on HTTP. The response varies by op; the typed
1906
+ * helpers below return each op's shape. */
1907
+ async aclDef(input, opts) {
1908
+ if (input.op === "names")
1909
+ return this.listAclDefNames(opts);
1910
+ return postJSON(this.ctx, this.aclPath("/v1/_acldef", opts?.tenant), input, opts);
1911
+ }
1912
+ /** Every rule list of the tenant, stored and config-declared, plus the
1913
+ * built-in baselines (`aclDef` op `list`). Retired lists only with
1914
+ * `includeRetired`. */
1915
+ async listAclDefs(opts) {
1916
+ const input = { op: "list" };
1917
+ if (opts?.includeRetired)
1918
+ input.include_retired = true;
1919
+ return (await this.aclDef(input, opts));
1920
+ }
1921
+ /** One rule list, retired or not (`aclDef` op `get`). A config-declared
1922
+ * list is returned with `source: "config"`. NotFoundError when absent. */
1923
+ async getAclDef(name, opts) {
1924
+ return (await this.aclDef({ op: "get", name }, opts));
1925
+ }
1926
+ /** Create or replace a rule list by name (`aclDef` op `put`): bumps its
1927
+ * version and un-retires it. The rules are validated first; a list the
1928
+ * engine could not enforce is refused with 400. A delegated writer may
1929
+ * not rewrite a list bound to itself (403 `acl_self_elevation`). */
1930
+ async putAclDef(body, opts) {
1931
+ const input = { op: "put", name: body.name, rules: body.rules };
1932
+ if (body.description !== undefined)
1933
+ input.description = body.description;
1934
+ return (await this.aclDef(input, opts));
1935
+ }
1936
+ /** Retire a rule list (`aclDef` op `retire`). Bindings to it stay and match
1937
+ * nothing; a later `putAclDef` of the name brings it back. NotFoundError
1938
+ * when there is no live list of that name. */
1939
+ async retireAclDef(name, opts) {
1940
+ return (await this.aclDef({ op: "retire", name }, opts));
1941
+ }
1942
+ /** Check rules without storing them (`aclDef` op `validate`). Rules the
1943
+ * engine refuses raise InvalidArgumentError (code `acl_invalid`) naming
1944
+ * the fault. */
1945
+ async validateAclRules(rules, opts) {
1946
+ return (await this.aclDef({ op: "validate", rules }, opts));
1947
+ }
1948
+ /** The resource types, verbs and transports a rule may name (`aclDef` op
1949
+ * `catalog`). */
1950
+ async getAclCatalog(opts) {
1951
+ return (await this.aclDef({ op: "catalog" }, opts));
1952
+ }
1953
+ /** Every live rule-list name a binding in the tenant can reach, stored and
1954
+ * config-declared, sorted (`GET /v1/_acldef/names`). */
1955
+ async listAclDefNames(opts) {
1956
+ return jsonFetch(this.ctx, this.aclPath("/v1/_acldef/names", opts?.tenant), opts);
1957
+ }
1958
+ /** Every binding of the tenant, stored and config-declared
1959
+ * (`GET /v1/_aclbinding`). Retired ones only with `includeRetired`. */
1960
+ async listAclBindings(opts) {
1961
+ const extra = opts?.includeRetired ? { include_retired: "1" } : undefined;
1962
+ return jsonFetch(this.ctx, this.aclPath("/v1/_aclbinding", opts?.tenant, extra), opts);
1963
+ }
1964
+ /** Bind a rule list to a subject (`POST /v1/_aclbinding`). The list must
1965
+ * exist and `expires_at` must be in the future, or InvalidArgumentError
1966
+ * (code `acl_binding_invalid`). A delegated writer may not bind itself, a
1967
+ * group it is in, its token or its tenant (403 `acl_self_elevation`). An
1968
+ * empty `subject_id` / `expires_at` is left out of the request. */
1969
+ async createAclBinding(body, opts) {
1970
+ const out = { acl: body.acl, subject_kind: body.subject_kind };
1971
+ if (body.subject_id)
1972
+ out.subject_id = body.subject_id;
1973
+ if (body.expires_at)
1974
+ out.expires_at = body.expires_at;
1975
+ return postJSON(this.ctx, this.aclPath("/v1/_aclbinding", opts?.tenant), out, opts);
1976
+ }
1977
+ /** Retire a binding by its `binding_id` (`DELETE /v1/_aclbinding/{id}`).
1978
+ * NotFoundError when there is no live binding of that id. A config
1979
+ * binding has no id and cannot be retired here. */
1980
+ async deleteAclBinding(bindingId, opts) {
1981
+ return deleteJSON(this.ctx, this.aclPath(`/v1/_aclbinding/${encodeURIComponent(bindingId)}`, opts?.tenant), opts);
1982
+ }
1983
+ /** May the subject do this? (`POST /v1/_authz/evaluation`, AuthZEN 1.0).
1984
+ * Omit `subject` to ask about yourself. */
1985
+ async authzEvaluation(req, opts) {
1986
+ return postJSON(this.ctx, this.aclPath("/v1/_authz/evaluation", opts?.tenant), req, opts);
1987
+ }
1988
+ /** Up to 256 questions for one subject in one call
1989
+ * (`POST /v1/_authz/evaluations`); decisions come back in order. */
1990
+ async authzEvaluations(req, opts) {
1991
+ return postJSON(this.ctx, this.aclPath("/v1/_authz/evaluations", opts?.tenant), req, opts);
1992
+ }
1993
+ /** A decision with its working: every rule read for the resource's type
1994
+ * and why it did or did not match (`POST /v1/_authz/explain`). */
1995
+ async authzExplain(req, opts) {
1996
+ return postJSON(this.ctx, this.aclPath("/v1/_authz/explain", opts?.tenant), req, opts);
1997
+ }
1998
+ /** What a draft of lists and bindings would decide, beside what is decided
1999
+ * now (`POST /v1/_authz/simulate`). Nothing is written. */
2000
+ async authzSimulate(req, opts) {
2001
+ return postJSON(this.ctx, this.aclPath("/v1/_authz/simulate", opts?.tenant), req, opts);
2002
+ }
1831
2003
  // ---- Schedules admin (GET/POST /v1/_schedules/...) ----
1832
2004
  //
1833
2005
  // What the Web UI's schedules page drives: the merged list, one schedule's
package/dist/errors.d.ts CHANGED
@@ -17,6 +17,7 @@
17
17
  * that behavior. PR 5b switches `runStreaming` + every new method
18
18
  * to raise typed errors via `raiseFromResponse`.
19
19
  */
20
+ import type { AuthzReason } from "./types.js";
20
21
  export declare class LoomcycleError extends Error {
21
22
  readonly status?: number;
22
23
  readonly bodyText?: string;
@@ -253,6 +254,34 @@ export declare class InsufficientScopeError extends LoomcycleError {
253
254
  requiredScope?: string;
254
255
  });
255
256
  }
257
+ /** An HTTP 403 because an access-control list refused the call: body code
258
+ * `acl_denied`. The token's scopes would allow it, but a rule bound to the
259
+ * caller (to its subject, a group it is in, its token or its tenant) denies
260
+ * it, or, where the deployment enforces lists, no rule allows it.
261
+ * `reasons` names the rules that decided — empty when none did — so an
262
+ * operator can find the list to change with `authzExplain`. Not retryable:
263
+ * the same call is refused until a list or binding changes.
264
+ *
265
+ * Other access-control refusals (`acl_self_elevation`, `authz_subject`,
266
+ * `operator_plane`, …) stay a plain LoomcycleError with that `code`. */
267
+ export declare class AclDeniedError extends LoomcycleError {
268
+ /** The verb asked, e.g. `read`, `author`, `manage`. Null when the body did
269
+ * not carry one. */
270
+ readonly action: string | null;
271
+ /** The resource asked about, `type:name` (e.g. `def:acl/ops`). Null when
272
+ * the body did not carry one. */
273
+ readonly resource: string | null;
274
+ /** The rules that decided, denies first; empty when no rule matched. */
275
+ readonly reasons: AuthzReason[];
276
+ constructor(message: string, opts?: {
277
+ status?: number;
278
+ bodyText?: string;
279
+ code?: string;
280
+ action?: string;
281
+ resource?: string;
282
+ reasons?: AuthzReason[];
283
+ });
284
+ }
256
285
  /** Every HTTP 503. The status is shared between a condition that clears on
257
286
  * its own and features this deployment does not have
258
287
  * (`decision_not_configured`, `store_unavailable`, `vector_unsupported`, …),
package/dist/errors.js CHANGED
@@ -268,6 +268,33 @@ export class InsufficientScopeError extends LoomcycleError {
268
268
  this.requiredScope = opts?.requiredScope ?? null;
269
269
  }
270
270
  }
271
+ /** An HTTP 403 because an access-control list refused the call: body code
272
+ * `acl_denied`. The token's scopes would allow it, but a rule bound to the
273
+ * caller (to its subject, a group it is in, its token or its tenant) denies
274
+ * it, or, where the deployment enforces lists, no rule allows it.
275
+ * `reasons` names the rules that decided — empty when none did — so an
276
+ * operator can find the list to change with `authzExplain`. Not retryable:
277
+ * the same call is refused until a list or binding changes.
278
+ *
279
+ * Other access-control refusals (`acl_self_elevation`, `authz_subject`,
280
+ * `operator_plane`, …) stay a plain LoomcycleError with that `code`. */
281
+ export class AclDeniedError extends LoomcycleError {
282
+ /** The verb asked, e.g. `read`, `author`, `manage`. Null when the body did
283
+ * not carry one. */
284
+ action;
285
+ /** The resource asked about, `type:name` (e.g. `def:acl/ops`). Null when
286
+ * the body did not carry one. */
287
+ resource;
288
+ /** The rules that decided, denies first; empty when no rule matched. */
289
+ reasons;
290
+ constructor(message, opts) {
291
+ super(message, opts);
292
+ this.name = "AclDeniedError";
293
+ this.action = opts?.action ?? null;
294
+ this.resource = opts?.resource ?? null;
295
+ this.reasons = opts?.reasons ?? [];
296
+ }
297
+ }
271
298
  /** Every HTTP 503. The status is shared between a condition that clears on
272
299
  * its own and features this deployment does not have
273
300
  * (`decision_not_configured`, `store_unavailable`, `vector_unsupported`, …),
@@ -79,6 +79,7 @@ export declare function deleteJSON<T>(ctx: _FetchContext, path: string, opts?: {
79
79
  * 401 → AuthError
80
80
  * 403 + code "insufficient_scope" (or an older server's
81
81
  * WWW-Authenticate scope hint) → InsufficientScopeError
82
+ * 403 + code "acl_denied" → AclDeniedError (action, resource, reasons)
82
83
  * 403 (other) → LoomcycleError (base)
83
84
  * 404 + "snapshot" → SnapshotNotFoundError ────────┐
84
85
  * 404 + "session" → SessionNotFoundError │ All extend
@@ -86,6 +87,7 @@ export declare function deleteJSON<T>(ctx: _FetchContext, path: string, opts?: {
86
87
  * 404 + (other) → NotFoundError (base) │ catch any 404
87
88
  * │ with one
88
89
  * │ instanceof.
90
+ * 409 + an access-control code (group_external, *_exists) → LoomcycleError
89
91
  * 409 + "already_pausing" / "already paused" → AlreadyPausingError
90
92
  * 409 + "not_paused" / "not paused" → NotPausedError
91
93
  * 409 + "session" → SessionBusyError
@@ -9,7 +9,7 @@
9
9
  * Method-level code in client.ts stays focused on URL + body shape;
10
10
  * the boring fetch + error-translation machinery lives here.
11
11
  */
12
- import { AgentIDInUseError, AgentNotFoundError, AlreadyPausingError, AuthError, BackpressureError, ChannelCursorRegressionError, InsufficientScopeError, InvalidArgumentError, LoomcycleError, NotFoundError, NotPausedError, PauseNotConfiguredError, PerUserQuotaExhaustedError, PromptTooLargeError, RateLimitedError, RequestTooLargeError, SessionBusyError, SessionNotFoundError, SnapshotNotFoundError, SnapshotTooLargeError, SnapshotVersionError, SubstrateToolRefusedError, TokenLimitExceededError, UnavailableError, } from "./errors.js";
12
+ import { AclDeniedError, AgentIDInUseError, AgentNotFoundError, AlreadyPausingError, AuthError, BackpressureError, ChannelCursorRegressionError, InsufficientScopeError, InvalidArgumentError, LoomcycleError, NotFoundError, NotPausedError, PauseNotConfiguredError, PerUserQuotaExhaustedError, PromptTooLargeError, RateLimitedError, RequestTooLargeError, SessionBusyError, SessionNotFoundError, SnapshotNotFoundError, SnapshotTooLargeError, SnapshotVersionError, SubstrateToolRefusedError, TokenLimitExceededError, UnavailableError, } from "./errors.js";
13
13
  /** authHeaders builds the standard request header set: JSON Accept
14
14
  * + Bearer token when the client was constructed with one. The
15
15
  * caller adds Content-Type when posting a body. */
@@ -159,6 +159,7 @@ export async function deleteJSON(ctx, path, opts) {
159
159
  * 401 → AuthError
160
160
  * 403 + code "insufficient_scope" (or an older server's
161
161
  * WWW-Authenticate scope hint) → InsufficientScopeError
162
+ * 403 + code "acl_denied" → AclDeniedError (action, resource, reasons)
162
163
  * 403 (other) → LoomcycleError (base)
163
164
  * 404 + "snapshot" → SnapshotNotFoundError ────────┐
164
165
  * 404 + "session" → SessionNotFoundError │ All extend
@@ -166,6 +167,7 @@ export async function deleteJSON(ctx, path, opts) {
166
167
  * 404 + (other) → NotFoundError (base) │ catch any 404
167
168
  * │ with one
168
169
  * │ instanceof.
170
+ * 409 + an access-control code (group_external, *_exists) → LoomcycleError
169
171
  * 409 + "already_pausing" / "already paused" → AlreadyPausingError
170
172
  * 409 + "not_paused" / "not paused" → NotPausedError
171
173
  * 409 + "session" → SessionBusyError
@@ -230,6 +232,15 @@ export async function raiseFromResponse(resp) {
230
232
  requiredScope: stringField(bodyObject(bodyText)?.required_scope) ?? hinted,
231
233
  });
232
234
  }
235
+ if (opts.code === "acl_denied") {
236
+ const body = bodyObject(bodyText) ?? {};
237
+ throw new AclDeniedError(msg, {
238
+ ...opts,
239
+ action: stringField(body.action),
240
+ resource: stringField(body.resource),
241
+ reasons: aclReasons(body.reasons),
242
+ });
243
+ }
233
244
  throw new LoomcycleError(msg, opts);
234
245
  }
235
246
  case 404:
@@ -248,6 +259,11 @@ export async function raiseFromResponse(resp) {
248
259
  throw new AgentNotFoundError(msg, opts);
249
260
  throw new NotFoundError(msg, opts);
250
261
  case 409:
262
+ // The access-control conflicts carry a code, and their messages quote
263
+ // caller-chosen names (a group called "session-ops"), which the
264
+ // keyword matches below would misread.
265
+ if (opts.code !== undefined && ACL_CONFLICT_CODES.has(opts.code))
266
+ throw new LoomcycleError(msg, opts);
251
267
  if (bodyLower.includes("already_pausing") || bodyLower.includes("already paused"))
252
268
  throw new AlreadyPausingError(msg, opts);
253
269
  if (bodyLower.includes("not_paused") || bodyLower.includes("not paused"))
@@ -339,6 +355,39 @@ export async function raiseFromResponse(resp) {
339
355
  throw new LoomcycleError(msg, opts);
340
356
  }
341
357
  }
358
+ /** The 409 codes of the access-control management routes: a group an
359
+ * identity provider owns, and a row or membership that already exists. */
360
+ const ACL_CONFLICT_CODES = new Set([
361
+ "group_external",
362
+ "group_exists",
363
+ "group_member_exists",
364
+ "acl_exists",
365
+ "acl_binding_exists",
366
+ ]);
367
+ /** aclReasons reads an `acl_denied` body's `reasons`, keeping only entries
368
+ * with the fields a reason always has: a proxy's or an older server's
369
+ * stray value must not surface as a rule. */
370
+ function aclReasons(v) {
371
+ if (!Array.isArray(v))
372
+ return [];
373
+ const out = [];
374
+ for (const r of v) {
375
+ if (r === null || typeof r !== "object")
376
+ continue;
377
+ const o = r;
378
+ const acl = stringField(o.acl);
379
+ const rule = numberField(o.rule);
380
+ const effect = o.effect;
381
+ if (acl === undefined || rule === undefined || (effect !== "allow" && effect !== "deny"))
382
+ continue;
383
+ const reason = { acl, rule, effect };
384
+ const version = numberField(o.version);
385
+ if (version !== undefined)
386
+ reason.version = version;
387
+ out.push(reason);
388
+ }
389
+ return out;
390
+ }
342
391
  /** retryAfterMs reads a `Retry-After` header given in seconds, as the server
343
392
  * always sends it, and returns milliseconds; undefined when the header is
344
393
  * absent or not a number (an HTTP-date form is not parsed). */