@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/README.md +50 -2
- package/dist/cjs/client.js +174 -2
- package/dist/cjs/errors.js +29 -1
- package/dist/cjs/fetch-helpers.js +49 -0
- package/dist/cjs/index.js +14 -2
- package/dist/client.d.ts +173 -3
- package/dist/client.js +174 -2
- package/dist/errors.d.ts +29 -0
- package/dist/errors.js +27 -0
- package/dist/fetch-helpers.d.ts +2 -0
- package/dist/fetch-helpers.js +50 -1
- package/dist/index.d.ts +14 -3
- package/dist/index.js +13 -2
- package/dist/types.d.ts +480 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -501,12 +501,58 @@ One method per substrate family returns every declared name with its version rol
|
|
|
501
501
|
| `listWebhookDefNames(opts?)` | `GET /v1/_webhookdef/names` |
|
|
502
502
|
| `listMemoryBackendDefNames(opts?)` | `GET /v1/_memorybackenddef/names` |
|
|
503
503
|
| `listDocumentSourceDefNames(opts?)` | `GET /v1/_documentsourcedef/names` |
|
|
504
|
-
| `listOperatorTokenDefNames(opts?)` | `GET /v1/_operatortokendef/names` — **admin token only**; `{name, tenant_id, subject, token_count, has_current, last_updated}`, never a token. |
|
|
504
|
+
| `listOperatorTokenDefNames(opts?)` | `GET /v1/_operatortokendef/names` — **admin token only**; one row per (tenant, name): `{name, tenant_id, subject, token_count, has_current, current_def_id?, last_updated}`, never a token. Rotate or retire by `current_def_id`; a per-user (`u-`) name is refused as a target. |
|
|
505
505
|
|
|
506
506
|
All but the last return `DefNamesResponse`; `live_version_count` / `active_retired` are reported by the AgentDef, SkillDef, HookDef and MCPServerDef families. Teams have `listTeams()`.
|
|
507
507
|
|
|
508
508
|
Team options ride on existing methods: `createTeam(name, overlay, { promote, description })` sends the create's own fields (omitted, a create is promoted — the server default; `promote: false` authors it without putting it in force), `forkTeam(name, overlay, { promote, parentDefId, description })` sends the fork's (omitted, the fork is authored but not active — the server default for a fork), and `runTeam({ ..., interruptOnCap: true })` asks a human whether to continue, reroute or abort when a state hits its iteration cap, instead of ending with `iteration_cap`.
|
|
509
509
|
|
|
510
|
+
### Access control (RFC EY / EZ)
|
|
511
|
+
|
|
512
|
+
Groups, access-control rule lists, the bindings that attach a list to a subject, and the AuthZEN-shaped decision questions. Groups, lists and bindings are the tenant operator's (`substrate:tenant`, an admin, or the legacy shared token; any other principal gets an `InsufficientScopeError`); the decision questions are open to any principal about itself. Every call acts in your own tenant; `opts.tenant` focuses another for an admin and is ignored for a tenant token. Rows the config declares (`acls:` / `groups:` / `acl_bindings:`) are listed beside the stored ones with `source: "config"`, without versions or timestamps, and cannot be written here. Every call needs a persistent store and is refused from inside a run.
|
|
513
|
+
|
|
514
|
+
| Method | Route | Returns |
|
|
515
|
+
|---|---|---|
|
|
516
|
+
| `listGroups(opts?)` | `GET /v1/_groups` | `GroupListResponse` — stored and config groups, by name |
|
|
517
|
+
| `getGroup(name, opts?)` | `GET /v1/_groups/{name}` | `GroupDetail` — a stored group, its direct members, `contained_in`; a config-only group is a `NotFoundError` |
|
|
518
|
+
| `getGroupEffective(name, opts?)` | `GET /v1/_groups/{name}/effective` | `GroupEffective` — every user through nested groups, config members included |
|
|
519
|
+
| `createGroup({name, description?, external_id?}, opts?)` | `POST /v1/_groups` | `GroupRow` — an upsert by name |
|
|
520
|
+
| `deleteGroup(name, opts?)` | `DELETE /v1/_groups/{name}` | `{deleted}` |
|
|
521
|
+
| `addGroupMember(name, {kind, id}, opts?)` | `POST /v1/_groups/{name}/members` | `GroupMemberChange`; `kind` is `user` or `group` (nesting) |
|
|
522
|
+
| `removeGroupMember(name, kind, id, opts?)` | `DELETE /v1/_groups/{name}/members/{kind}/{id}` | `GroupMemberChange` with `removed: true` |
|
|
523
|
+
| `aclDef(input, opts?)` | `POST /v1/_acldef` | the op's body; `op: "names"` reads `GET /v1/_acldef/names` |
|
|
524
|
+
| `listAclDefs({includeRetired?}?)` | `aclDef` op `list` | `AclDefListResponse` — `acls` plus the read-only `builtin` baselines |
|
|
525
|
+
| `getAclDef(name, opts?)` | op `get` | `AclDefRow` |
|
|
526
|
+
| `putAclDef({name, rules, description?}, opts?)` | op `put` | `AclDefRow` — creates or replaces, bumping `version` |
|
|
527
|
+
| `retireAclDef(name, opts?)` | op `retire` | `{retired}` |
|
|
528
|
+
| `validateAclRules(rules, opts?)` | op `validate` | `AclValidateResult`; bad rules are an `InvalidArgumentError` with code `acl_invalid` |
|
|
529
|
+
| `getAclCatalog(opts?)` | op `catalog` | `AclCatalog` — types, verbs, scopes, transports |
|
|
530
|
+
| `listAclDefNames(opts?)` | `GET /v1/_acldef/names` | `{names}` |
|
|
531
|
+
| `listAclBindings({includeRetired?}?)` | `GET /v1/_aclbinding` | `AclBindingListResponse` |
|
|
532
|
+
| `createAclBinding({acl, subject_kind, subject_id?, expires_at?}, opts?)` | `POST /v1/_aclbinding` | `AclBinding`; a missing list or a past expiry is an `InvalidArgumentError` with code `acl_binding_invalid` |
|
|
533
|
+
| `deleteAclBinding(bindingId, opts?)` | `DELETE /v1/_aclbinding/{id}` | `{retired}` |
|
|
534
|
+
| `authzEvaluation(req, opts?)` | `POST /v1/_authz/evaluation` | `AuthzDecision` — `decision` is true only when a rule allowed |
|
|
535
|
+
| `authzEvaluations(req, opts?)` | `POST /v1/_authz/evaluations` | `{evaluations}` — up to 256, in order |
|
|
536
|
+
| `authzExplain(req, opts?)` | `POST /v1/_authz/explain` | `AuthzExplainResponse` — every rule read and why |
|
|
537
|
+
| `authzSimulate(req, opts?)` | `POST /v1/_authz/simulate` | `AuthzSimulateResponse` — `now` and `with_draft` per request; nothing is written |
|
|
538
|
+
|
|
539
|
+
`whoami()` also reports `acl_mode` (`compat` / `shadow` / `enforce`), the caller's `groups` and `acl_version`, each absent when the server does not send it. A delegated writer changing its own authority is refused with code `acl_self_elevation`; a write to a group an identity provider owns (`external_id` set) with a 409 `group_external`. A rule that refuses any call — here or on any other route — throws `AclDeniedError` (see [Errors](#errors)).
|
|
540
|
+
|
|
541
|
+
```ts
|
|
542
|
+
const { names } = await client.listAclDefNames();
|
|
543
|
+
await client.putAclDef({
|
|
544
|
+
name: "support-tier-1",
|
|
545
|
+
rules: [{ effect: "deny", actions: ["invoke"], resources: ["tool:Bash"] }],
|
|
546
|
+
});
|
|
547
|
+
await client.createAclBinding({ acl: "support-tier-1", subject_kind: "group", subject_id: "support" });
|
|
548
|
+
const why = await client.authzExplain({
|
|
549
|
+
subject: { type: "user", id: "alice" },
|
|
550
|
+
action: { name: "invoke" },
|
|
551
|
+
resource: { type: "tool", id: "Bash" },
|
|
552
|
+
});
|
|
553
|
+
console.log(why.decision.outcome, why.traces);
|
|
554
|
+
```
|
|
555
|
+
|
|
510
556
|
### Decision models
|
|
511
557
|
|
|
512
558
|
Ask a **decision model** typed questions about a piece of text and get each answer with its probabilities. A decision model does not write: it reads the `state` you give it and answers a choice, a yes/no or a score, so there is no reply to parse and a call costs a few output tokens. Use it to route, gate, rank or grade text you already have. No run is involved; the tokens are charged to the caller and count against its token budget.
|
|
@@ -805,11 +851,13 @@ When the error body is JSON with a string `code` (`token_limit_exceeded`, `per_u
|
|
|
805
851
|
| 400 | `InvalidArgumentError` |
|
|
806
852
|
| 401 | `AuthError` |
|
|
807
853
|
| 403 + code `insufficient_scope` | `InsufficientScopeError` (not retryable; `e.requiredScope` names the scope the token lacks — from the body's `required_scope`, or from `WWW-Authenticate` on an older server that sent plain text) |
|
|
808
|
-
| 403
|
|
854
|
+
| 403 + code `acl_denied` | `AclDeniedError` (an access-control rule refused the call; `e.action`, `e.resource` and `e.reasons` — the rules that decided, denies first, empty when none matched; not retryable) |
|
|
855
|
+
| 403 (other) | `LoomcycleError` (base; e.g. `restricted_token`, `operator_key_restricted`, `acl_self_elevation`, `authz_subject` on `e.code`) |
|
|
809
856
|
| 404 + "snapshot" | `SnapshotNotFoundError` ⎫ |
|
|
810
857
|
| 404 + "session" | `SessionNotFoundError` ⎬ all extend `NotFoundError` |
|
|
811
858
|
| 404 + "agent" | `AgentNotFoundError` ⎬ |
|
|
812
859
|
| 404 (other) | `NotFoundError` (base) ⎭ catch any 404 with one `instanceof` |
|
|
860
|
+
| 409 + code `group_external` / `group_exists` / `group_member_exists` / `acl_exists` / `acl_binding_exists` | `LoomcycleError` (base; matched by code before the keywords below, which a group named e.g. `session-ops` would trip) |
|
|
813
861
|
| 409 + "already_pausing" / "already paused" | `AlreadyPausingError` |
|
|
814
862
|
| 409 + "not_paused" / "not paused" | `NotPausedError` |
|
|
815
863
|
| 409 + "session" | `SessionBusyError` |
|
package/dist/cjs/client.js
CHANGED
|
@@ -1499,6 +1499,8 @@ class LoomcycleClient {
|
|
|
1499
1499
|
};
|
|
1500
1500
|
if (opts.headers)
|
|
1501
1501
|
overlay.headers = opts.headers;
|
|
1502
|
+
if (opts.binaryResults)
|
|
1503
|
+
overlay.binary_results = opts.binaryResults;
|
|
1502
1504
|
const createInput = { op: "create", name: opts.name, overlay };
|
|
1503
1505
|
if (opts.description)
|
|
1504
1506
|
createInput.description = opts.description;
|
|
@@ -1824,13 +1826,183 @@ class LoomcycleClient {
|
|
|
1824
1826
|
async listDocumentSourceDefNames(opts) {
|
|
1825
1827
|
return this.listDefNames("documentsourcedef", opts);
|
|
1826
1828
|
}
|
|
1827
|
-
/** Every OperatorTokenDef name (`GET /v1/_operatortokendef/names`),
|
|
1828
|
-
*
|
|
1829
|
+
/** Every OperatorTokenDef name (`GET /v1/_operatortokendef/names`), one row
|
|
1830
|
+
* per (tenant, name), with its subject, token counts and current def_id —
|
|
1831
|
+
* never a token. **Admin token only**: token
|
|
1829
1832
|
* minting stays operator-only, so a `substrate:tenant` token is refused
|
|
1830
1833
|
* with 403. */
|
|
1831
1834
|
async listOperatorTokenDefNames(opts) {
|
|
1832
1835
|
return this.listDefNames("operatortokendef", opts);
|
|
1833
1836
|
}
|
|
1837
|
+
// ---- Access control: groups, rule lists, bindings, decisions (RFC EY / EZ) ----
|
|
1838
|
+
//
|
|
1839
|
+
// Groups, lists and bindings are the tenant operator's: a `substrate:tenant`
|
|
1840
|
+
// token, an admin, or the legacy shared token. Any other principal is
|
|
1841
|
+
// refused with 403 (InsufficientScopeError). Every call acts in the caller's
|
|
1842
|
+
// own tenant; `tenant` is an admin's focus on another one and is ignored for
|
|
1843
|
+
// a tenant operator. Rows the config declares are listed beside the stored
|
|
1844
|
+
// ones with `source: "config"`; they cannot be written here. Every write
|
|
1845
|
+
// takes effect on every replica at once. A rule that refuses a call throws
|
|
1846
|
+
// AclDeniedError (403 `acl_denied`) naming the rules that decided.
|
|
1847
|
+
//
|
|
1848
|
+
// The decision questions (authzEvaluation / authzEvaluations / authzExplain
|
|
1849
|
+
// / authzSimulate) are open to any principal about itself; naming another
|
|
1850
|
+
// subject of the tenant needs a tenant operator (403 `authz_subject`).
|
|
1851
|
+
// Every call here needs a persistent store (503 otherwise) and is refused
|
|
1852
|
+
// from inside a run.
|
|
1853
|
+
/** `path` with the admin tenant focus and any extra query parameters. */
|
|
1854
|
+
aclPath(path, tenant, extra) {
|
|
1855
|
+
const q = new URLSearchParams(extra);
|
|
1856
|
+
if (tenant)
|
|
1857
|
+
q.set("tenant", tenant);
|
|
1858
|
+
const qs = q.toString();
|
|
1859
|
+
return qs ? `${path}?${qs}` : path;
|
|
1860
|
+
}
|
|
1861
|
+
/** Every group of the tenant, stored and config-declared, sorted by name
|
|
1862
|
+
* (`GET /v1/_groups`). */
|
|
1863
|
+
async listGroups(opts) {
|
|
1864
|
+
return (0, fetch_helpers_js_1.jsonFetch)(this.ctx, this.aclPath("/v1/_groups", opts?.tenant), opts);
|
|
1865
|
+
}
|
|
1866
|
+
/** One stored group with its direct members and the groups it is inside of
|
|
1867
|
+
* (`GET /v1/_groups/{name}`). A config-only group is not stored and raises
|
|
1868
|
+
* NotFoundError; {@link LoomcycleClient.getGroupEffective} covers it. */
|
|
1869
|
+
async getGroup(name, opts) {
|
|
1870
|
+
return (0, fetch_helpers_js_1.jsonFetch)(this.ctx, this.aclPath(`/v1/_groups/${encodeURIComponent(name)}`, opts?.tenant), opts);
|
|
1871
|
+
}
|
|
1872
|
+
/** Every user the group holds, through nested groups at any depth and
|
|
1873
|
+
* config-declared members included (`GET /v1/_groups/{name}/effective`). */
|
|
1874
|
+
async getGroupEffective(name, opts) {
|
|
1875
|
+
return (0, fetch_helpers_js_1.jsonFetch)(this.ctx, this.aclPath(`/v1/_groups/${encodeURIComponent(name)}/effective`, opts?.tenant), opts);
|
|
1876
|
+
}
|
|
1877
|
+
/** Create a group, or update its description and `external_id` if the name
|
|
1878
|
+
* exists (`POST /v1/_groups`). A group with an `external_id` is owned by an
|
|
1879
|
+
* identity provider: a later write to it raises a 409 LoomcycleError with
|
|
1880
|
+
* code `group_external`. */
|
|
1881
|
+
async createGroup(body, opts) {
|
|
1882
|
+
return (0, fetch_helpers_js_1.postJSON)(this.ctx, this.aclPath("/v1/_groups", opts?.tenant), body, opts);
|
|
1883
|
+
}
|
|
1884
|
+
/** Delete a group and every membership edge naming it
|
|
1885
|
+
* (`DELETE /v1/_groups/{name}`). Bindings to it stay and apply to no one.
|
|
1886
|
+
* NotFoundError when there is no such group. */
|
|
1887
|
+
async deleteGroup(name, opts) {
|
|
1888
|
+
return (0, fetch_helpers_js_1.deleteJSON)(this.ctx, this.aclPath(`/v1/_groups/${encodeURIComponent(name)}`, opts?.tenant), opts);
|
|
1889
|
+
}
|
|
1890
|
+
/** Add a user or a group to a group (`POST /v1/_groups/{name}/members`). A
|
|
1891
|
+
* group member nests: its members are in this group too. The server
|
|
1892
|
+
* refuses an edge that would close a cycle or nest too deep (400), one
|
|
1893
|
+
* that exists (409 `group_member_exists`), and a delegated writer adding
|
|
1894
|
+
* itself (403 `acl_self_elevation`). */
|
|
1895
|
+
async addGroupMember(name, member, opts) {
|
|
1896
|
+
return (0, fetch_helpers_js_1.postJSON)(this.ctx, this.aclPath(`/v1/_groups/${encodeURIComponent(name)}/members`, opts?.tenant), { kind: member.kind, id: member.id }, opts);
|
|
1897
|
+
}
|
|
1898
|
+
/** Remove one membership edge
|
|
1899
|
+
* (`DELETE /v1/_groups/{name}/members/{kind}/{id}`). NotFoundError when
|
|
1900
|
+
* there is no such edge. */
|
|
1901
|
+
async removeGroupMember(name, kind, id, opts) {
|
|
1902
|
+
const path = `/v1/_groups/${encodeURIComponent(name)}/members/${encodeURIComponent(kind)}/${encodeURIComponent(id)}`;
|
|
1903
|
+
return (0, fetch_helpers_js_1.deleteJSON)(this.ctx, this.aclPath(path, opts?.tenant), opts);
|
|
1904
|
+
}
|
|
1905
|
+
/** The AclDef family, op-dispatched — the same ops the gRPC `AclDef` RPC
|
|
1906
|
+
* and the MCP `acldef` tool take. Every op but `names` posts to
|
|
1907
|
+
* `POST /v1/_acldef`; `names` reads `GET /v1/_acldef/names`, which is
|
|
1908
|
+
* where that op lives on HTTP. The response varies by op; the typed
|
|
1909
|
+
* helpers below return each op's shape. */
|
|
1910
|
+
async aclDef(input, opts) {
|
|
1911
|
+
if (input.op === "names")
|
|
1912
|
+
return this.listAclDefNames(opts);
|
|
1913
|
+
return (0, fetch_helpers_js_1.postJSON)(this.ctx, this.aclPath("/v1/_acldef", opts?.tenant), input, opts);
|
|
1914
|
+
}
|
|
1915
|
+
/** Every rule list of the tenant, stored and config-declared, plus the
|
|
1916
|
+
* built-in baselines (`aclDef` op `list`). Retired lists only with
|
|
1917
|
+
* `includeRetired`. */
|
|
1918
|
+
async listAclDefs(opts) {
|
|
1919
|
+
const input = { op: "list" };
|
|
1920
|
+
if (opts?.includeRetired)
|
|
1921
|
+
input.include_retired = true;
|
|
1922
|
+
return (await this.aclDef(input, opts));
|
|
1923
|
+
}
|
|
1924
|
+
/** One rule list, retired or not (`aclDef` op `get`). A config-declared
|
|
1925
|
+
* list is returned with `source: "config"`. NotFoundError when absent. */
|
|
1926
|
+
async getAclDef(name, opts) {
|
|
1927
|
+
return (await this.aclDef({ op: "get", name }, opts));
|
|
1928
|
+
}
|
|
1929
|
+
/** Create or replace a rule list by name (`aclDef` op `put`): bumps its
|
|
1930
|
+
* version and un-retires it. The rules are validated first; a list the
|
|
1931
|
+
* engine could not enforce is refused with 400. A delegated writer may
|
|
1932
|
+
* not rewrite a list bound to itself (403 `acl_self_elevation`). */
|
|
1933
|
+
async putAclDef(body, opts) {
|
|
1934
|
+
const input = { op: "put", name: body.name, rules: body.rules };
|
|
1935
|
+
if (body.description !== undefined)
|
|
1936
|
+
input.description = body.description;
|
|
1937
|
+
return (await this.aclDef(input, opts));
|
|
1938
|
+
}
|
|
1939
|
+
/** Retire a rule list (`aclDef` op `retire`). Bindings to it stay and match
|
|
1940
|
+
* nothing; a later `putAclDef` of the name brings it back. NotFoundError
|
|
1941
|
+
* when there is no live list of that name. */
|
|
1942
|
+
async retireAclDef(name, opts) {
|
|
1943
|
+
return (await this.aclDef({ op: "retire", name }, opts));
|
|
1944
|
+
}
|
|
1945
|
+
/** Check rules without storing them (`aclDef` op `validate`). Rules the
|
|
1946
|
+
* engine refuses raise InvalidArgumentError (code `acl_invalid`) naming
|
|
1947
|
+
* the fault. */
|
|
1948
|
+
async validateAclRules(rules, opts) {
|
|
1949
|
+
return (await this.aclDef({ op: "validate", rules }, opts));
|
|
1950
|
+
}
|
|
1951
|
+
/** The resource types, verbs and transports a rule may name (`aclDef` op
|
|
1952
|
+
* `catalog`). */
|
|
1953
|
+
async getAclCatalog(opts) {
|
|
1954
|
+
return (await this.aclDef({ op: "catalog" }, opts));
|
|
1955
|
+
}
|
|
1956
|
+
/** Every live rule-list name a binding in the tenant can reach, stored and
|
|
1957
|
+
* config-declared, sorted (`GET /v1/_acldef/names`). */
|
|
1958
|
+
async listAclDefNames(opts) {
|
|
1959
|
+
return (0, fetch_helpers_js_1.jsonFetch)(this.ctx, this.aclPath("/v1/_acldef/names", opts?.tenant), opts);
|
|
1960
|
+
}
|
|
1961
|
+
/** Every binding of the tenant, stored and config-declared
|
|
1962
|
+
* (`GET /v1/_aclbinding`). Retired ones only with `includeRetired`. */
|
|
1963
|
+
async listAclBindings(opts) {
|
|
1964
|
+
const extra = opts?.includeRetired ? { include_retired: "1" } : undefined;
|
|
1965
|
+
return (0, fetch_helpers_js_1.jsonFetch)(this.ctx, this.aclPath("/v1/_aclbinding", opts?.tenant, extra), opts);
|
|
1966
|
+
}
|
|
1967
|
+
/** Bind a rule list to a subject (`POST /v1/_aclbinding`). The list must
|
|
1968
|
+
* exist and `expires_at` must be in the future, or InvalidArgumentError
|
|
1969
|
+
* (code `acl_binding_invalid`). A delegated writer may not bind itself, a
|
|
1970
|
+
* group it is in, its token or its tenant (403 `acl_self_elevation`). An
|
|
1971
|
+
* empty `subject_id` / `expires_at` is left out of the request. */
|
|
1972
|
+
async createAclBinding(body, opts) {
|
|
1973
|
+
const out = { acl: body.acl, subject_kind: body.subject_kind };
|
|
1974
|
+
if (body.subject_id)
|
|
1975
|
+
out.subject_id = body.subject_id;
|
|
1976
|
+
if (body.expires_at)
|
|
1977
|
+
out.expires_at = body.expires_at;
|
|
1978
|
+
return (0, fetch_helpers_js_1.postJSON)(this.ctx, this.aclPath("/v1/_aclbinding", opts?.tenant), out, opts);
|
|
1979
|
+
}
|
|
1980
|
+
/** Retire a binding by its `binding_id` (`DELETE /v1/_aclbinding/{id}`).
|
|
1981
|
+
* NotFoundError when there is no live binding of that id. A config
|
|
1982
|
+
* binding has no id and cannot be retired here. */
|
|
1983
|
+
async deleteAclBinding(bindingId, opts) {
|
|
1984
|
+
return (0, fetch_helpers_js_1.deleteJSON)(this.ctx, this.aclPath(`/v1/_aclbinding/${encodeURIComponent(bindingId)}`, opts?.tenant), opts);
|
|
1985
|
+
}
|
|
1986
|
+
/** May the subject do this? (`POST /v1/_authz/evaluation`, AuthZEN 1.0).
|
|
1987
|
+
* Omit `subject` to ask about yourself. */
|
|
1988
|
+
async authzEvaluation(req, opts) {
|
|
1989
|
+
return (0, fetch_helpers_js_1.postJSON)(this.ctx, this.aclPath("/v1/_authz/evaluation", opts?.tenant), req, opts);
|
|
1990
|
+
}
|
|
1991
|
+
/** Up to 256 questions for one subject in one call
|
|
1992
|
+
* (`POST /v1/_authz/evaluations`); decisions come back in order. */
|
|
1993
|
+
async authzEvaluations(req, opts) {
|
|
1994
|
+
return (0, fetch_helpers_js_1.postJSON)(this.ctx, this.aclPath("/v1/_authz/evaluations", opts?.tenant), req, opts);
|
|
1995
|
+
}
|
|
1996
|
+
/** A decision with its working: every rule read for the resource's type
|
|
1997
|
+
* and why it did or did not match (`POST /v1/_authz/explain`). */
|
|
1998
|
+
async authzExplain(req, opts) {
|
|
1999
|
+
return (0, fetch_helpers_js_1.postJSON)(this.ctx, this.aclPath("/v1/_authz/explain", opts?.tenant), req, opts);
|
|
2000
|
+
}
|
|
2001
|
+
/** What a draft of lists and bindings would decide, beside what is decided
|
|
2002
|
+
* now (`POST /v1/_authz/simulate`). Nothing is written. */
|
|
2003
|
+
async authzSimulate(req, opts) {
|
|
2004
|
+
return (0, fetch_helpers_js_1.postJSON)(this.ctx, this.aclPath("/v1/_authz/simulate", opts?.tenant), req, opts);
|
|
2005
|
+
}
|
|
1834
2006
|
// ---- Schedules admin (GET/POST /v1/_schedules/...) ----
|
|
1835
2007
|
//
|
|
1836
2008
|
// What the Web UI's schedules page drives: the merged list, one schedule's
|
package/dist/cjs/errors.js
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* to raise typed errors via `raiseFromResponse`.
|
|
20
20
|
*/
|
|
21
21
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
-
exports.SubstrateToolRefusedError = exports.ChannelCursorRegressionError = exports.SnapshotVersionError = exports.PromptTooLargeError = exports.RequestTooLargeError = exports.SnapshotTooLargeError = exports.SnapshotNotFoundError = exports.NotPausedError = exports.AlreadyPausingError = exports.PauseNotConfiguredError = exports.InvalidArgumentError = exports.UnavailableError = exports.InsufficientScopeError = exports.AuthError = exports.PerUserQuotaExhaustedError = exports.TokenLimitExceededError = exports.RateLimitedError = exports.BackpressureError = exports.AgentIDInUseError = exports.SessionBusyError = exports.SessionNotFoundError = exports.AgentNotFoundError = exports.NotFoundError = exports.LoomcycleError = void 0;
|
|
22
|
+
exports.SubstrateToolRefusedError = exports.ChannelCursorRegressionError = exports.SnapshotVersionError = exports.PromptTooLargeError = exports.RequestTooLargeError = exports.SnapshotTooLargeError = exports.SnapshotNotFoundError = exports.NotPausedError = exports.AlreadyPausingError = exports.PauseNotConfiguredError = exports.InvalidArgumentError = exports.UnavailableError = exports.AclDeniedError = exports.InsufficientScopeError = exports.AuthError = exports.PerUserQuotaExhaustedError = exports.TokenLimitExceededError = exports.RateLimitedError = exports.BackpressureError = exports.AgentIDInUseError = exports.SessionBusyError = exports.SessionNotFoundError = exports.AgentNotFoundError = exports.NotFoundError = exports.LoomcycleError = void 0;
|
|
23
23
|
class LoomcycleError extends Error {
|
|
24
24
|
status;
|
|
25
25
|
bodyText;
|
|
@@ -283,6 +283,34 @@ class InsufficientScopeError extends LoomcycleError {
|
|
|
283
283
|
}
|
|
284
284
|
}
|
|
285
285
|
exports.InsufficientScopeError = InsufficientScopeError;
|
|
286
|
+
/** An HTTP 403 because an access-control list refused the call: body code
|
|
287
|
+
* `acl_denied`. The token's scopes would allow it, but a rule bound to the
|
|
288
|
+
* caller (to its subject, a group it is in, its token or its tenant) denies
|
|
289
|
+
* it, or, where the deployment enforces lists, no rule allows it.
|
|
290
|
+
* `reasons` names the rules that decided — empty when none did — so an
|
|
291
|
+
* operator can find the list to change with `authzExplain`. Not retryable:
|
|
292
|
+
* the same call is refused until a list or binding changes.
|
|
293
|
+
*
|
|
294
|
+
* Other access-control refusals (`acl_self_elevation`, `authz_subject`,
|
|
295
|
+
* `operator_plane`, …) stay a plain LoomcycleError with that `code`. */
|
|
296
|
+
class AclDeniedError extends LoomcycleError {
|
|
297
|
+
/** The verb asked, e.g. `read`, `author`, `manage`. Null when the body did
|
|
298
|
+
* not carry one. */
|
|
299
|
+
action;
|
|
300
|
+
/** The resource asked about, `type:name` (e.g. `def:acl/ops`). Null when
|
|
301
|
+
* the body did not carry one. */
|
|
302
|
+
resource;
|
|
303
|
+
/** The rules that decided, denies first; empty when no rule matched. */
|
|
304
|
+
reasons;
|
|
305
|
+
constructor(message, opts) {
|
|
306
|
+
super(message, opts);
|
|
307
|
+
this.name = "AclDeniedError";
|
|
308
|
+
this.action = opts?.action ?? null;
|
|
309
|
+
this.resource = opts?.resource ?? null;
|
|
310
|
+
this.reasons = opts?.reasons ?? [];
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
exports.AclDeniedError = AclDeniedError;
|
|
286
314
|
/** Every HTTP 503. The status is shared between a condition that clears on
|
|
287
315
|
* its own and features this deployment does not have
|
|
288
316
|
* (`decision_not_configured`, `store_unavailable`, `vector_unsupported`, …),
|
|
@@ -169,6 +169,7 @@ async function deleteJSON(ctx, path, opts) {
|
|
|
169
169
|
* 401 → AuthError
|
|
170
170
|
* 403 + code "insufficient_scope" (or an older server's
|
|
171
171
|
* WWW-Authenticate scope hint) → InsufficientScopeError
|
|
172
|
+
* 403 + code "acl_denied" → AclDeniedError (action, resource, reasons)
|
|
172
173
|
* 403 (other) → LoomcycleError (base)
|
|
173
174
|
* 404 + "snapshot" → SnapshotNotFoundError ────────┐
|
|
174
175
|
* 404 + "session" → SessionNotFoundError │ All extend
|
|
@@ -176,6 +177,7 @@ async function deleteJSON(ctx, path, opts) {
|
|
|
176
177
|
* 404 + (other) → NotFoundError (base) │ catch any 404
|
|
177
178
|
* │ with one
|
|
178
179
|
* │ instanceof.
|
|
180
|
+
* 409 + an access-control code (group_external, *_exists) → LoomcycleError
|
|
179
181
|
* 409 + "already_pausing" / "already paused" → AlreadyPausingError
|
|
180
182
|
* 409 + "not_paused" / "not paused" → NotPausedError
|
|
181
183
|
* 409 + "session" → SessionBusyError
|
|
@@ -240,6 +242,15 @@ async function raiseFromResponse(resp) {
|
|
|
240
242
|
requiredScope: stringField(bodyObject(bodyText)?.required_scope) ?? hinted,
|
|
241
243
|
});
|
|
242
244
|
}
|
|
245
|
+
if (opts.code === "acl_denied") {
|
|
246
|
+
const body = bodyObject(bodyText) ?? {};
|
|
247
|
+
throw new errors_js_1.AclDeniedError(msg, {
|
|
248
|
+
...opts,
|
|
249
|
+
action: stringField(body.action),
|
|
250
|
+
resource: stringField(body.resource),
|
|
251
|
+
reasons: aclReasons(body.reasons),
|
|
252
|
+
});
|
|
253
|
+
}
|
|
243
254
|
throw new errors_js_1.LoomcycleError(msg, opts);
|
|
244
255
|
}
|
|
245
256
|
case 404:
|
|
@@ -258,6 +269,11 @@ async function raiseFromResponse(resp) {
|
|
|
258
269
|
throw new errors_js_1.AgentNotFoundError(msg, opts);
|
|
259
270
|
throw new errors_js_1.NotFoundError(msg, opts);
|
|
260
271
|
case 409:
|
|
272
|
+
// The access-control conflicts carry a code, and their messages quote
|
|
273
|
+
// caller-chosen names (a group called "session-ops"), which the
|
|
274
|
+
// keyword matches below would misread.
|
|
275
|
+
if (opts.code !== undefined && ACL_CONFLICT_CODES.has(opts.code))
|
|
276
|
+
throw new errors_js_1.LoomcycleError(msg, opts);
|
|
261
277
|
if (bodyLower.includes("already_pausing") || bodyLower.includes("already paused"))
|
|
262
278
|
throw new errors_js_1.AlreadyPausingError(msg, opts);
|
|
263
279
|
if (bodyLower.includes("not_paused") || bodyLower.includes("not paused"))
|
|
@@ -349,6 +365,39 @@ async function raiseFromResponse(resp) {
|
|
|
349
365
|
throw new errors_js_1.LoomcycleError(msg, opts);
|
|
350
366
|
}
|
|
351
367
|
}
|
|
368
|
+
/** The 409 codes of the access-control management routes: a group an
|
|
369
|
+
* identity provider owns, and a row or membership that already exists. */
|
|
370
|
+
const ACL_CONFLICT_CODES = new Set([
|
|
371
|
+
"group_external",
|
|
372
|
+
"group_exists",
|
|
373
|
+
"group_member_exists",
|
|
374
|
+
"acl_exists",
|
|
375
|
+
"acl_binding_exists",
|
|
376
|
+
]);
|
|
377
|
+
/** aclReasons reads an `acl_denied` body's `reasons`, keeping only entries
|
|
378
|
+
* with the fields a reason always has: a proxy's or an older server's
|
|
379
|
+
* stray value must not surface as a rule. */
|
|
380
|
+
function aclReasons(v) {
|
|
381
|
+
if (!Array.isArray(v))
|
|
382
|
+
return [];
|
|
383
|
+
const out = [];
|
|
384
|
+
for (const r of v) {
|
|
385
|
+
if (r === null || typeof r !== "object")
|
|
386
|
+
continue;
|
|
387
|
+
const o = r;
|
|
388
|
+
const acl = stringField(o.acl);
|
|
389
|
+
const rule = numberField(o.rule);
|
|
390
|
+
const effect = o.effect;
|
|
391
|
+
if (acl === undefined || rule === undefined || (effect !== "allow" && effect !== "deny"))
|
|
392
|
+
continue;
|
|
393
|
+
const reason = { acl, rule, effect };
|
|
394
|
+
const version = numberField(o.version);
|
|
395
|
+
if (version !== undefined)
|
|
396
|
+
reason.version = version;
|
|
397
|
+
out.push(reason);
|
|
398
|
+
}
|
|
399
|
+
return out;
|
|
400
|
+
}
|
|
352
401
|
/** retryAfterMs reads a `Retry-After` header given in seconds, as the server
|
|
353
402
|
* always sends it, and returns milliseconds; undefined when the header is
|
|
354
403
|
* absent or not a number (an HTTP-date form is not parsed). */
|
package/dist/cjs/index.js
CHANGED
|
@@ -95,6 +95,15 @@
|
|
|
95
95
|
* (opts?): Promise<DefNamesResponse>
|
|
96
96
|
* listOperatorTokenDefNames(opts?): Promise<OperatorTokenDefNamesResponse> // admin token only
|
|
97
97
|
*
|
|
98
|
+
* // Access control (RFC EY / EZ; tenant operator; opts.tenant = admin focus)
|
|
99
|
+
* listGroups / getGroup(name) / getGroupEffective(name) / createGroup(body) /
|
|
100
|
+
* deleteGroup(name) / addGroupMember(name, {kind, id}) / removeGroupMember(name, kind, id)
|
|
101
|
+
* aclDef(input): Promise<unknown> // op-dispatched; names → GET /v1/_acldef/names
|
|
102
|
+
* listAclDefs / getAclDef(name) / putAclDef(body) / retireAclDef(name) /
|
|
103
|
+
* validateAclRules(rules) / getAclCatalog() / listAclDefNames()
|
|
104
|
+
* listAclBindings / createAclBinding(body) / deleteAclBinding(bindingId)
|
|
105
|
+
* authzEvaluation(req) / authzEvaluations(req) / authzExplain(req) / authzSimulate(req) // any principal, about itself
|
|
106
|
+
*
|
|
98
107
|
* // Schedules admin
|
|
99
108
|
* listAllSchedules(opts?): Promise<ScheduleListResponse> // static yaml + ScheduleDefs, merged
|
|
100
109
|
* getScheduleState(defId): Promise<ScheduleStateView>
|
|
@@ -159,7 +168,9 @@
|
|
|
159
168
|
* SnapshotTooLargeError, RequestTooLargeError + PromptTooLargeError
|
|
160
169
|
* (both subclasses of SnapshotTooLargeError), TokenLimitExceededError
|
|
161
170
|
* (NOT a BackpressureError: a hard budget, not retryable),
|
|
162
|
-
* SnapshotVersionError, SubstrateToolRefusedError (v0.8.22)
|
|
171
|
+
* SnapshotVersionError, SubstrateToolRefusedError (v0.8.22),
|
|
172
|
+
* AclDeniedError (a 403 an access-control rule decided; action,
|
|
173
|
+
* resource, reasons).
|
|
163
174
|
* Every one carries `retryable` (true only for a refusal that clears
|
|
164
175
|
* on its own).
|
|
165
176
|
*
|
|
@@ -171,7 +182,7 @@
|
|
|
171
182
|
* See `adapters/ts/README.md` for usage examples.
|
|
172
183
|
*/
|
|
173
184
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
174
|
-
exports.UnavailableError = exports.TokenLimitExceededError = exports.SubstrateToolRefusedError = exports.SnapshotVersionError = exports.SnapshotTooLargeError = exports.SnapshotNotFoundError = exports.SessionNotFoundError = exports.SessionBusyError = exports.RequestTooLargeError = exports.RateLimitedError = exports.PromptTooLargeError = exports.PerUserQuotaExhaustedError = exports.PauseNotConfiguredError = exports.NotPausedError = exports.LoomcycleError = exports.ChannelCursorRegressionError = exports.InvalidArgumentError = exports.InsufficientScopeError = exports.NotFoundError = exports.BackpressureError = exports.AuthError = exports.AlreadyPausingError = exports.AgentNotFoundError = exports.AgentIDInUseError = exports.CLIENT_TOOL_SUBPROTOCOL = exports.clientToolsURL = exports.ClientToolHost = exports.verifyLoomcycleDelivery = exports.verifyLoomcycleSignature = exports.InteractiveSession = exports.LoomcycleClient = void 0;
|
|
185
|
+
exports.UnavailableError = exports.TokenLimitExceededError = exports.SubstrateToolRefusedError = exports.SnapshotVersionError = exports.SnapshotTooLargeError = exports.SnapshotNotFoundError = exports.SessionNotFoundError = exports.SessionBusyError = exports.RequestTooLargeError = exports.RateLimitedError = exports.PromptTooLargeError = exports.PerUserQuotaExhaustedError = exports.PauseNotConfiguredError = exports.NotPausedError = exports.LoomcycleError = exports.ChannelCursorRegressionError = exports.InvalidArgumentError = exports.InsufficientScopeError = exports.NotFoundError = exports.BackpressureError = exports.AuthError = exports.AlreadyPausingError = exports.AgentNotFoundError = exports.AgentIDInUseError = exports.AclDeniedError = exports.CLIENT_TOOL_SUBPROTOCOL = exports.clientToolsURL = exports.ClientToolHost = exports.verifyLoomcycleDelivery = exports.verifyLoomcycleSignature = exports.InteractiveSession = exports.LoomcycleClient = void 0;
|
|
175
186
|
var client_js_1 = require("./client.js");
|
|
176
187
|
Object.defineProperty(exports, "LoomcycleClient", { enumerable: true, get: function () { return client_js_1.LoomcycleClient; } });
|
|
177
188
|
var interactive_js_1 = require("./interactive.js");
|
|
@@ -184,6 +195,7 @@ Object.defineProperty(exports, "ClientToolHost", { enumerable: true, get: functi
|
|
|
184
195
|
Object.defineProperty(exports, "clientToolsURL", { enumerable: true, get: function () { return client_tools_js_1.clientToolsURL; } });
|
|
185
196
|
Object.defineProperty(exports, "CLIENT_TOOL_SUBPROTOCOL", { enumerable: true, get: function () { return client_tools_js_1.CLIENT_TOOL_SUBPROTOCOL; } });
|
|
186
197
|
var errors_js_1 = require("./errors.js");
|
|
198
|
+
Object.defineProperty(exports, "AclDeniedError", { enumerable: true, get: function () { return errors_js_1.AclDeniedError; } });
|
|
187
199
|
Object.defineProperty(exports, "AgentIDInUseError", { enumerable: true, get: function () { return errors_js_1.AgentIDInUseError; } });
|
|
188
200
|
Object.defineProperty(exports, "AgentNotFoundError", { enumerable: true, get: function () { return errors_js_1.AgentNotFoundError; } });
|
|
189
201
|
Object.defineProperty(exports, "AlreadyPausingError", { enumerable: true, get: function () { return errors_js_1.AlreadyPausingError; } });
|