nexus-ai-pro 2.13.0 → 2.14.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/API_STABILITY.md +30 -0
- package/CHANGELOG.md +44 -1
- package/README.md +11 -6
- package/dist/batch/anthropic.js +3 -1
- package/dist/batch/manager.d.ts +5 -0
- package/dist/batch/manager.js +25 -9
- package/dist/batch/openai-client.js +3 -1
- package/dist/cli/policy.d.ts +10 -0
- package/dist/cli/policy.js +116 -0
- package/dist/cli.js +15 -7
- package/dist/core/lifecycle.d.ts +23 -3
- package/dist/core/lifecycle.js +76 -13
- package/dist/core/nexus.d.ts +14 -14
- package/dist/core/nexus.js +26 -26
- package/dist/embeddings/batch-google.js +7 -1
- package/dist/embeddings/batch.d.ts +7 -0
- package/dist/embeddings/batch.js +32 -9
- package/dist/embeddings/engine.js +1 -1
- package/dist/evaluate/emulate.d.ts +143 -0
- package/dist/evaluate/emulate.js +214 -0
- package/dist/images/operation.js +1 -1
- package/dist/lifecycle/continue.d.ts +12 -0
- package/dist/lifecycle/continue.js +14 -0
- package/dist/media/index.d.ts +14 -1
- package/dist/media/index.js +16 -6
- package/dist/media/mistral.js +3 -1
- package/dist/policy/index.d.ts +184 -0
- package/dist/policy/index.js +490 -0
- package/dist/realtime/gemini-live.transport.js +3 -0
- package/dist/realtime/openai-websocket.transport.d.ts +1 -0
- package/dist/realtime/openai-websocket.transport.js +12 -7
- package/dist/realtime/session.js +8 -2
- package/dist/realtime/types.d.ts +8 -1
- package/dist/telephony/manager.js +21 -15
- package/dist/telephony/providers/telnyx.d.ts +14 -13
- package/dist/telephony/providers/telnyx.js +36 -20
- package/dist/telephony/providers/twilio.d.ts +18 -13
- package/dist/telephony/providers/twilio.js +48 -24
- package/dist/types/batch.d.ts +5 -0
- package/dist/types/config.d.ts +3 -1
- package/dist/types/lifecycle.d.ts +40 -0
- package/dist/types/residency.d.ts +19 -7
- package/dist/types/telephony.d.ts +26 -0
- package/dist/types/voice.d.ts +4 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/video/google.js +11 -2
- package/dist/video/index.d.ts +10 -0
- package/dist/video/index.js +39 -14
- package/dist/video/runway.js +4 -1
- package/dist/voice/manager.js +14 -2
- package/dist/voice/providers/openai.d.ts +3 -2
- package/dist/voice/providers/openai.js +9 -10
- package/dist-cjs/batch/anthropic.js +3 -1
- package/dist-cjs/batch/manager.d.ts +5 -0
- package/dist-cjs/batch/manager.js +25 -9
- package/dist-cjs/batch/openai-client.js +3 -1
- package/dist-cjs/cli/policy.d.ts +10 -0
- package/dist-cjs/cli/policy.js +122 -0
- package/dist-cjs/core/lifecycle.d.ts +23 -3
- package/dist-cjs/core/lifecycle.js +77 -13
- package/dist-cjs/core/nexus.d.ts +14 -14
- package/dist-cjs/core/nexus.js +34 -34
- package/dist-cjs/embeddings/batch-google.js +7 -1
- package/dist-cjs/embeddings/batch.d.ts +7 -0
- package/dist-cjs/embeddings/batch.js +32 -9
- package/dist-cjs/embeddings/engine.js +1 -1
- package/dist-cjs/evaluate/emulate.d.ts +143 -0
- package/dist-cjs/evaluate/emulate.js +221 -0
- package/dist-cjs/images/operation.js +1 -1
- package/dist-cjs/lifecycle/continue.d.ts +12 -0
- package/dist-cjs/lifecycle/continue.js +17 -0
- package/dist-cjs/media/index.d.ts +14 -1
- package/dist-cjs/media/index.js +16 -6
- package/dist-cjs/media/mistral.js +3 -1
- package/dist-cjs/policy/index.d.ts +184 -0
- package/dist-cjs/policy/index.js +500 -0
- package/dist-cjs/realtime/gemini-live.transport.js +3 -0
- package/dist-cjs/realtime/openai-websocket.transport.d.ts +1 -0
- package/dist-cjs/realtime/openai-websocket.transport.js +12 -7
- package/dist-cjs/realtime/session.js +8 -2
- package/dist-cjs/realtime/types.d.ts +8 -1
- package/dist-cjs/telephony/manager.js +21 -15
- package/dist-cjs/telephony/providers/telnyx.d.ts +14 -13
- package/dist-cjs/telephony/providers/telnyx.js +36 -20
- package/dist-cjs/telephony/providers/twilio.d.ts +18 -13
- package/dist-cjs/telephony/providers/twilio.js +48 -24
- package/dist-cjs/types/batch.d.ts +5 -0
- package/dist-cjs/types/config.d.ts +3 -1
- package/dist-cjs/types/lifecycle.d.ts +40 -0
- package/dist-cjs/types/residency.d.ts +19 -7
- package/dist-cjs/types/telephony.d.ts +26 -0
- package/dist-cjs/types/voice.d.ts +4 -0
- package/dist-cjs/version.d.ts +1 -1
- package/dist-cjs/version.js +1 -1
- package/dist-cjs/video/google.js +11 -2
- package/dist-cjs/video/index.d.ts +10 -0
- package/dist-cjs/video/index.js +39 -14
- package/dist-cjs/video/runway.js +4 -1
- package/dist-cjs/voice/manager.js +14 -2
- package/dist-cjs/voice/providers/openai.d.ts +3 -2
- package/dist-cjs/voice/providers/openai.js +9 -10
- package/llms.txt +3 -2
- package/package.json +27 -1
package/API_STABILITY.md
CHANGED
|
@@ -41,6 +41,36 @@ Both are covered by this policy:
|
|
|
41
41
|
Deep imports into `dist`, `dist-cjs`, or `src` are not supported. Import only from `nexus-ai-pro` or
|
|
42
42
|
one of its explicit subpaths.
|
|
43
43
|
|
|
44
|
+
## 2.14 stage (2.14.0)
|
|
45
|
+
|
|
46
|
+
Everything 2.14 adds is stable from the start and additive, unless this section says otherwise.
|
|
47
|
+
|
|
48
|
+
New and stable:
|
|
49
|
+
- `nexus-ai-pro/policy`: `parsePolicy()`, `applyPolicy()`, `comparePolicies()`, `checkPolicyChange()`,
|
|
50
|
+
`providerGate()`, `policyHash()`, `PolicyError`, `Policy`, `PolicyDocument`, `PolicyAccess`,
|
|
51
|
+
`PolicyRouting`, `PolicyBudgets`, `PolicyResidency`, `PolicyCredentials`, `PolicyIssue`,
|
|
52
|
+
`ApplyPolicyOptions`, `PolicyChange`, `PolicyChangeKind`, and `PolicyCheck`;
|
|
53
|
+
- `LifecycleConfig.providers` and `policy`, `ProviderGate`, `PolicyReference`,
|
|
54
|
+
`OperationDescriptor.policy`, `AuditLogEvent.policy`, and `OperationLifecycle.access()`;
|
|
55
|
+
- the `nexus policy check` command, its flags, its exit codes, and its `--json` output;
|
|
56
|
+
- `nexus-ai-pro/evaluate/emulate`: `emulatedTools()`, `EmulatedToolsOptions`, `EmulatorClient`,
|
|
57
|
+
`EmulatedToolCall`, `EmulatedResult`, `EmulationStore`, `MemoryEmulationStore`, `EmulationError`, and
|
|
58
|
+
`emulationKey()`. What the emulator is told may be reworded in a minor release; a store's keys never
|
|
59
|
+
change within a major;
|
|
60
|
+
- `CallResidency.refusal()`, optional, and the optional `model` its `allows()`, `place()`, and
|
|
61
|
+
`check()` take;
|
|
62
|
+
- `OperationLifecycle.callContext()`, the optional `OperationLifecycleLike.callContext`, and
|
|
63
|
+
`ContinuedCallContext`;
|
|
64
|
+
- `tenantId` on `BatchSubmitRequest`, `EmbeddingBatchSubmitRequest`, `MediaReadRequest`,
|
|
65
|
+
`MediaDescribeRequest`, `MediaAnswerRequest`, `TranscriptionRequest`, `SpeechRequest`, and every
|
|
66
|
+
telephony request that takes a `signal`; `MediaCallContext.tenantId` and `credential`;
|
|
67
|
+
`RealtimeSessionConfig.credential`; the optional context the OpenAI voice adapter and the Twilio and
|
|
68
|
+
Telnyx adapters take on each method that calls their API.
|
|
69
|
+
|
|
70
|
+
The kinds `comparePolicies()` gives a change may be refined in a minor release: a change found to widen
|
|
71
|
+
what a tenant may reach is reported as `widened` from then on, so a check may start to ask for an
|
|
72
|
+
acknowledgment it did not ask for before. A check never stops asking for one it asked for.
|
|
73
|
+
|
|
44
74
|
## 2.13 stage (2.13.0)
|
|
45
75
|
|
|
46
76
|
Everything 2.13 adds is stable from the start and additive, unless this section says otherwise.
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,48 @@ Notable changes to this project are recorded here. The format follows [Keep a Ch
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [2.14.0] - 2026-10-19
|
|
8
|
+
|
|
9
|
+
Policy you can review, evaluation without side effects, and credentials per tenant in every family.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **Policy you can review.** `nexus-ai-pro/policy` reads guardrails, routing, budgets, residency, and
|
|
14
|
+
credentials from one versioned file. `parsePolicy()` validates it as the client starts, naming every
|
|
15
|
+
issue; `applyPolicy()` turns it into the client's configuration; and every operation and audit event
|
|
16
|
+
records the policy's name, version, and content hash. `nexus policy check` compares a version with
|
|
17
|
+
the one before it, a file or `git:<ref>`, and fails the build on a change that widens what a tenant
|
|
18
|
+
may reach (a region, a provider, a model, a budget, lowered guardrails, a shared key) until it is
|
|
19
|
+
acknowledged in the file or with `--accept`, and on content changed without a new version.
|
|
20
|
+
- **Per-tenant routing rules.** `LifecycleConfig.providers` takes a `ProviderGate` that admits or
|
|
21
|
+
refuses each provider and model an operation would reach, checked wherever residency is: a refused
|
|
22
|
+
provider is refused with an `OperationDeniedError` naming the rule before anything is sent, a
|
|
23
|
+
completion's refused attempt is dropped from its route, and an embedding never falls back to a
|
|
24
|
+
refused model. `providerGate()` builds one from a policy's routing rules, with `*` wildcards.
|
|
25
|
+
- **Evaluation without side effects.** `emulatedTools()`, on the new `nexus-ai-pro/evaluate/emulate`,
|
|
26
|
+
returns an agent's tools answered by a model playing each one, told the tool, its result schema, a
|
|
27
|
+
scenario, real calls, and the run's earlier calls, and never run, so an evaluation of an agent that
|
|
28
|
+
charges cards or writes to a database sends nothing. Answers are kept by tool, arguments, scenario,
|
|
29
|
+
and call, and replayed, so a suite runs the same answers in CI without a model; tools that change
|
|
30
|
+
nothing can run for real.
|
|
31
|
+
- **Credentials per tenant in every family.** Video, media understanding, completion and embedding
|
|
32
|
+
batches, voice, telephony, and realtime sessions send the tenant's key, resolved by
|
|
33
|
+
`LifecycleConfig.credentials`, in place of the configured one, as completions, embeddings, and images
|
|
34
|
+
have since 2.13. Each takes `tenantId` on its requests. A video job's or a batch's ref records its
|
|
35
|
+
tenant, so `status()`, `resume()`, `cancel()`, and `download()` use the tenant's key and regions from
|
|
36
|
+
any process, through the new `OperationLifecycle.callContext()`. A Twilio tenant's calls go out on its
|
|
37
|
+
subaccount; an OpenAI SIP call keeps its project's key.
|
|
38
|
+
- `ai.lifecycle.access()` returns the `CallResidency` an operation would get, and
|
|
39
|
+
`CallResidency.refusal()` says why a provider would be refused once placed on its twin; `allows()`,
|
|
40
|
+
`place()`, and `check()` take the model as well.
|
|
41
|
+
|
|
42
|
+
### Fixed
|
|
43
|
+
|
|
44
|
+
- `VideoManager.start()` and the calls on a video ref, and `EmbeddingBatchManager.start()` and the
|
|
45
|
+
calls on its refs, ran outside the client's residency policy; they are held to the tenant's regions
|
|
46
|
+
now. A durable video generation ran its operation without its tenant, so a tenant's budget and rate
|
|
47
|
+
limit did not apply to it.
|
|
48
|
+
|
|
7
49
|
## [2.13.0] - 2026-10-19
|
|
8
50
|
|
|
9
51
|
Regions and residency, and credentials per tenant.
|
|
@@ -2946,7 +2988,8 @@ shared policy vocabulary is in place for a future release that revisits this.
|
|
|
2946
2988
|
- URL fetching rejects unsafe private-network targets and limits response reads.
|
|
2947
2989
|
- CLI and audit findings no longer disclose detected secret values.
|
|
2948
2990
|
|
|
2949
|
-
[Unreleased]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.
|
|
2991
|
+
[Unreleased]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.14.0...HEAD
|
|
2992
|
+
[2.14.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.13.0...v2.14.0
|
|
2950
2993
|
[2.13.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.12.0...v2.13.0
|
|
2951
2994
|
[2.12.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.11.0...v2.12.0
|
|
2952
2995
|
[2.11.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.10.0...v2.11.0
|
package/README.md
CHANGED
|
@@ -141,7 +141,7 @@ from the doc comments. The guides live in the repository, so these links go to G
|
|
|
141
141
|
| [MCP](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/mcp.md) | Borrowing tools from MCP servers, a registry of many, and serving your own |
|
|
142
142
|
| [Agent protocols](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/protocols.md) | Serving an agent to user interfaces over AG-UI, to other agents over A2A, and to code editors over ACP, and calling remote agents as tools |
|
|
143
143
|
| [Traces](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/tracing.md) | Run trees, queries, feedback, comparison, and alerts |
|
|
144
|
-
| [Evaluation](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/evaluation.md) | Datasets, evaluators, experiments, comparisons with a verdict, review queues, LLM judges |
|
|
144
|
+
| [Evaluation](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/evaluation.md) | Datasets, evaluators, experiments, comparisons with a verdict, review queues, LLM judges, and tools emulated by a model so an evaluation sends nothing |
|
|
145
145
|
| [Context hub](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/context-hub.md) | Prompts, instructions, tools, and skills versioned together, promoted through gates, and moved between projects |
|
|
146
146
|
| [Insights](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/insights.md) | Failing and slow runs clustered, regressions between time windows, and evaluated fix proposals |
|
|
147
147
|
| [Prompts](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/prompts.md) | Typed templates, content versions, gated promotion, rollback, A/B splits, serving through outages |
|
|
@@ -167,6 +167,7 @@ from the doc comments. The guides live in the repository, so these links go to G
|
|
|
167
167
|
| [Agent server](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/server.md) | Self-hosted HTTP server for assistants: threads, durable runs, resumable streams, cron, a worker queue, and metrics |
|
|
168
168
|
| [Deployments](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/deployments.md) | Revisions and canaries with automatic rollback, autoscaling on Kubernetes and Helm, and per-tenant limits |
|
|
169
169
|
| [Tenancy](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/tenancy.md) | One tenant model: a tenant's view of every store, tenant graph runs, and the isolation suite |
|
|
170
|
+
| [Policy](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/policy.md) | Guardrails, routing, budgets, residency, and credentials as one versioned file, validated as the client starts, checked by `nexus policy check` in CI, and recorded on every operation |
|
|
170
171
|
| [Regions and credentials](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/regions.md) | Agent servers across regions behind a gateway, residency rules that keep each tenant's requests, traces, and assets in its regions, and each tenant's provider keys from a secret manager |
|
|
171
172
|
| [Workflows](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/workflows.md) | Ready-made chains and domain workflows |
|
|
172
173
|
| [Command line](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/cli.md) | The `nexus` command |
|
|
@@ -312,14 +313,18 @@ Shipped so far:
|
|
|
312
313
|
- 2.13 regions and residency, and credentials per tenant: agent servers across regions behind a
|
|
313
314
|
gateway that fails over only within a tenant's regions, residency rules that keep each tenant's
|
|
314
315
|
provider calls, traces, and assets in its regions, and each tenant's provider keys from a secret
|
|
315
|
-
manager, rotated without a restart and kept out of every error
|
|
316
|
+
manager, rotated without a restart and kept out of every error;
|
|
317
|
+
- 2.14 policy you can review, and evaluation without side effects: guardrails, routing, budgets,
|
|
318
|
+
residency, and credentials as one versioned file that `nexus policy check` compares with the version
|
|
319
|
+
before it in CI and every operation records, tools emulated by a model so an evaluation sends
|
|
320
|
+
nothing, and each tenant's key in every family.
|
|
316
321
|
|
|
317
322
|
Upgrading from 1.x: `npx nexus migrate src --write` moves your imports, and
|
|
318
323
|
[MIGRATING.md](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/MIGRATING.md) covers the rest.
|
|
319
|
-
Next, 2.
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
324
|
+
Next, 2.15 publishes a compatibility record for community adapters: the contract suite each passed,
|
|
325
|
+
against which version, with its benchmark, on one page. Pinecone, images, the Gemini embedding batch
|
|
326
|
+
tier, media understanding, video, Gemini Live, SIP, and both phone providers graduate once the live
|
|
327
|
+
workflow passes against each. The
|
|
323
328
|
[roadmap](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/ROADMAP.md) lists what comes after, in the order
|
|
324
329
|
applications ask for it.
|
|
325
330
|
|
package/dist/batch/anthropic.js
CHANGED
|
@@ -127,13 +127,15 @@ export class AnthropicBatchProvider {
|
|
|
127
127
|
async send(path, init, context) {
|
|
128
128
|
const fetchImpl = this.options.fetch ?? fetch;
|
|
129
129
|
const baseUrl = (this.options.baseUrl || 'https://api.anthropic.com/v1').replace(/\/$/, '');
|
|
130
|
+
const credential = await context.credential?.(this.info.name);
|
|
130
131
|
const response = await fetchImpl(`${baseUrl}${path}`, {
|
|
131
132
|
...init,
|
|
132
133
|
headers: {
|
|
133
|
-
'x-api-key': this.options.apiKey,
|
|
134
|
+
'x-api-key': credential?.apiKey ?? this.options.apiKey,
|
|
134
135
|
'anthropic-version': this.options.version ?? '2023-06-01',
|
|
135
136
|
'content-type': 'application/json',
|
|
136
137
|
...this.options.headers,
|
|
138
|
+
...credential?.headers,
|
|
137
139
|
...(init.headers ?? {}),
|
|
138
140
|
},
|
|
139
141
|
signal: context.signal,
|
package/dist/batch/manager.d.ts
CHANGED
|
@@ -60,6 +60,11 @@ export declare class BatchManager {
|
|
|
60
60
|
status(ref: BatchJobRef): Promise<BatchJobState>;
|
|
61
61
|
/** Cancels a batch. Throws when the provider cannot cancel. */
|
|
62
62
|
cancel(ref: BatchJobRef): Promise<BatchJobState>;
|
|
63
|
+
/**
|
|
64
|
+
* A call on a batch outside its operation, as the tenant its ref records, or `tenantId` for a ref
|
|
65
|
+
* made before refs recorded one: with the tenant's key, inside its regions.
|
|
66
|
+
*/
|
|
67
|
+
private continuing;
|
|
63
68
|
private collect;
|
|
64
69
|
/**
|
|
65
70
|
* Prices a finished batch at the provider's discounted rate.
|
package/dist/batch/manager.js
CHANGED
|
@@ -2,6 +2,7 @@ import { OperationRunner } from '../operations/runner.js';
|
|
|
2
2
|
import { estimateCost } from '../optimizer/cost.js';
|
|
3
3
|
import { DEFAULT_CURRENCY } from '../optimizer/cost.js';
|
|
4
4
|
import { generateRequestId } from '../utils/ids.js';
|
|
5
|
+
import { continuing } from '../lifecycle/continue.js';
|
|
5
6
|
import { withLifecycle } from '../lifecycle/run.js';
|
|
6
7
|
import { BatchCapabilityError, BatchProviderNotFoundError, BatchValidationError } from './errors.js';
|
|
7
8
|
import { pollUntilSettled } from './poll.js';
|
|
@@ -46,6 +47,7 @@ export class BatchManager {
|
|
|
46
47
|
operation: 'batch.submit',
|
|
47
48
|
requestId,
|
|
48
49
|
provider: providerName,
|
|
50
|
+
...(request.tenantId !== undefined ? { tenantId: request.tenantId } : {}),
|
|
49
51
|
metadata: { items: request.items.length },
|
|
50
52
|
}, {
|
|
51
53
|
signal: operation.signal,
|
|
@@ -60,7 +62,7 @@ export class BatchManager {
|
|
|
60
62
|
};
|
|
61
63
|
let ref = operation.previousHeartbeat?.batchRef;
|
|
62
64
|
if (!ref) {
|
|
63
|
-
ref = await provider.submit(request, context);
|
|
65
|
+
ref = withTenant(await provider.submit(request, context), request.tenantId);
|
|
64
66
|
await operation.heartbeat({ batchRef: ref });
|
|
65
67
|
}
|
|
66
68
|
operation.report({ completed: 0, total: request.items.length, message: `submitted as ${ref.id}` });
|
|
@@ -77,34 +79,41 @@ export class BatchManager {
|
|
|
77
79
|
const provider = this.providers.get(ref.provider);
|
|
78
80
|
if (!provider)
|
|
79
81
|
throw new BatchProviderNotFoundError(ref.provider);
|
|
80
|
-
|
|
82
|
+
return this.continuing('batch.collect', ref, options.tenantId, (continued) => this.collect(provider, ref, options, undefined, {
|
|
83
|
+
...continued,
|
|
81
84
|
requestId: generateRequestId(),
|
|
82
85
|
signal: options.signal ?? new AbortController().signal,
|
|
83
86
|
attempt: 1,
|
|
84
|
-
};
|
|
85
|
-
return this.collect(provider, ref, options, undefined, context);
|
|
87
|
+
}));
|
|
86
88
|
}
|
|
87
89
|
async status(ref) {
|
|
88
90
|
const provider = this.providers.get(ref.provider);
|
|
89
91
|
if (!provider)
|
|
90
92
|
throw new BatchProviderNotFoundError(ref.provider);
|
|
91
|
-
return provider.poll(ref, {
|
|
93
|
+
return this.continuing('batch.status', ref, undefined, (continued) => provider.poll(ref, {
|
|
94
|
+
...continued,
|
|
92
95
|
requestId: generateRequestId(),
|
|
93
96
|
signal: new AbortController().signal,
|
|
94
97
|
attempt: 1,
|
|
95
|
-
});
|
|
98
|
+
}));
|
|
96
99
|
}
|
|
97
100
|
async cancel(ref) {
|
|
98
101
|
const provider = this.providers.get(ref.provider);
|
|
99
102
|
if (!provider)
|
|
100
103
|
throw new BatchProviderNotFoundError(ref.provider);
|
|
101
|
-
|
|
104
|
+
const cancel = provider.cancel;
|
|
105
|
+
if (!cancel)
|
|
102
106
|
throw new BatchCapabilityError(ref.provider, 'cancel');
|
|
103
|
-
return
|
|
107
|
+
return this.continuing('batch.cancel', ref, undefined, (continued) => cancel.call(provider, ref, {
|
|
108
|
+
...continued,
|
|
104
109
|
requestId: generateRequestId(),
|
|
105
110
|
signal: new AbortController().signal,
|
|
106
111
|
attempt: 1,
|
|
107
|
-
});
|
|
112
|
+
}));
|
|
113
|
+
}
|
|
114
|
+
continuing(operation, ref, tenantId, call) {
|
|
115
|
+
const tenant = tenantOf(ref) ?? tenantId;
|
|
116
|
+
return continuing(this.runtime.lifecycle, { family: 'batch', operation, provider: ref.provider, ...(tenant !== undefined ? { tenantId: tenant } : {}) }, call);
|
|
108
117
|
}
|
|
109
118
|
async collect(provider, ref, request, operation, context) {
|
|
110
119
|
const state = await pollUntilSettled(() => provider.poll(ref, context), {
|
|
@@ -230,3 +239,10 @@ function sumUsage(items) {
|
|
|
230
239
|
}
|
|
231
240
|
return { inputTokens, outputTokens, totalTokens: inputTokens + outputTokens };
|
|
232
241
|
}
|
|
242
|
+
function withTenant(ref, tenantId) {
|
|
243
|
+
return tenantId === undefined ? ref : { ...ref, metadata: { ...ref.metadata, batch: { tenantId } } };
|
|
244
|
+
}
|
|
245
|
+
function tenantOf(ref) {
|
|
246
|
+
const notes = ref.metadata?.batch;
|
|
247
|
+
return typeof notes?.tenantId === 'string' ? notes.tenantId : undefined;
|
|
248
|
+
}
|
|
@@ -84,10 +84,12 @@ export class OpenAIBatchClient {
|
|
|
84
84
|
async send(path, init, context) {
|
|
85
85
|
const fetchImpl = this.options.fetch ?? fetch;
|
|
86
86
|
const baseUrl = (this.options.baseUrl || 'https://api.openai.com/v1').replace(/\/$/, '');
|
|
87
|
+
const credential = await context.credential?.('openai');
|
|
87
88
|
const headers = {
|
|
88
|
-
authorization: `Bearer ${this.options.apiKey}`,
|
|
89
|
+
authorization: `Bearer ${credential?.apiKey ?? this.options.apiKey}`,
|
|
89
90
|
...(this.options.organization ? { 'openai-organization': this.options.organization } : {}),
|
|
90
91
|
...this.options.headers,
|
|
92
|
+
...credential?.headers,
|
|
91
93
|
...(init.headers ?? {}),
|
|
92
94
|
};
|
|
93
95
|
const response = await fetchImpl(`${baseUrl}${path}`, { ...init, headers, signal: context.signal });
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type ParsedArgs } from './args.js';
|
|
2
|
+
/**
|
|
3
|
+
* `nexus policy check <policy.json> [--base <file>|git:<ref>] [--accept path,path] [--json]`.
|
|
4
|
+
*
|
|
5
|
+
* Validates the policy, and, given the version it replaces, lists every change: each that widens what
|
|
6
|
+
* a tenant may reach, or that needs a reviewer's eye, must be acknowledged in the file's
|
|
7
|
+
* `acknowledged` or with `--accept`, and content that changed must come with a new version. Exits 1
|
|
8
|
+
* when the policy is not valid or a change is not acknowledged.
|
|
9
|
+
*/
|
|
10
|
+
export declare function runPolicyCommand(subcommand: string | undefined, args: ParsedArgs): Promise<void>;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
5
|
+
import { checkPolicyChange, PolicyError, parsePolicy } from '../policy/index.js';
|
|
6
|
+
import { CliUsageError, flagBool, listFlag, printTable, stringFlag, writeJson } from './args.js';
|
|
7
|
+
const run = promisify(execFile);
|
|
8
|
+
export async function runPolicyCommand(subcommand, args) {
|
|
9
|
+
if (subcommand !== 'check')
|
|
10
|
+
throw new CliUsageError(`Unknown policy command "${subcommand ?? ''}". Use check.`);
|
|
11
|
+
const [file] = args.positionals;
|
|
12
|
+
if (!file)
|
|
13
|
+
throw new CliUsageError('nexus policy check needs a policy file: nexus policy check policy.json [--base git:main].');
|
|
14
|
+
const json = flagBool(args.flags, 'json');
|
|
15
|
+
const base = stringFlag(args.flags, 'base');
|
|
16
|
+
const after = parse(await readFile(file, 'utf8'), file, json);
|
|
17
|
+
if (!after)
|
|
18
|
+
return;
|
|
19
|
+
if (base === undefined) {
|
|
20
|
+
report(json, { policy: after.reference, valid: true }, `${describe(after)} is valid.`);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const text = await readBase(base, file);
|
|
24
|
+
if (text === undefined) {
|
|
25
|
+
report(json, { policy: after.reference, valid: true, base: null }, `${describe(after)} is valid. ${base} has no policy at ${file}: nothing to compare.`);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const before = parse(text, base, json);
|
|
29
|
+
if (!before)
|
|
30
|
+
return;
|
|
31
|
+
const check = checkPolicyChange(before.document, after.document, { accept: listFlag(args.flags, 'accept') ?? [] });
|
|
32
|
+
if (!check.ok)
|
|
33
|
+
process.exitCode = 1;
|
|
34
|
+
if (json) {
|
|
35
|
+
writeJson({ policy: after.reference, valid: true, base: before.reference, ...check });
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
console.log(`${describe(after)} is valid. Compared with ${before.name}@${before.version} from ${base}:`);
|
|
39
|
+
if (check.changes.length === 0)
|
|
40
|
+
console.log('No changes.');
|
|
41
|
+
else {
|
|
42
|
+
const pending = new Set(check.unacknowledged);
|
|
43
|
+
printTable(check.changes.map((change) => ({
|
|
44
|
+
kind: change.kind,
|
|
45
|
+
path: change.path,
|
|
46
|
+
review: change.kind === 'narrowed' ? '' : pending.has(change) ? 'NOT ACKNOWLEDGED' : 'acknowledged',
|
|
47
|
+
summary: change.summary,
|
|
48
|
+
})), ['kind', 'path', 'review', 'summary']);
|
|
49
|
+
}
|
|
50
|
+
if (check.versionUnchanged) {
|
|
51
|
+
console.log('');
|
|
52
|
+
console.log(`The content changed, but the version is still ${after.version}: raise it.`);
|
|
53
|
+
}
|
|
54
|
+
if (check.unacknowledged.length > 0) {
|
|
55
|
+
const paths = check.unacknowledged.map((change) => change.path);
|
|
56
|
+
console.log('');
|
|
57
|
+
console.log(`${paths.length} change${paths.length === 1 ? '' : 's'} need${paths.length === 1 ? 's' : ''} acknowledging: add ${paths.map((item) => `"${item}"`).join(', ')} to "acknowledged" in ${file}, or pass --accept ${paths.join(',')}.`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function parse(text, source, json) {
|
|
61
|
+
try {
|
|
62
|
+
return parsePolicy(text);
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
if (!(error instanceof PolicyError))
|
|
66
|
+
throw error;
|
|
67
|
+
process.exitCode = 1;
|
|
68
|
+
if (json)
|
|
69
|
+
writeJson({ source, valid: false, issues: error.issues });
|
|
70
|
+
else {
|
|
71
|
+
console.log(`${source} is not a valid policy:`);
|
|
72
|
+
for (const issue of error.issues)
|
|
73
|
+
console.log(` ${issue.path || '(document)'}: ${issue.message}`);
|
|
74
|
+
}
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async function readBase(base, file) {
|
|
79
|
+
if (!base.startsWith('git:'))
|
|
80
|
+
return readFile(base, 'utf8');
|
|
81
|
+
const ref = base.slice(4);
|
|
82
|
+
if (!ref)
|
|
83
|
+
throw new CliUsageError('--base git: needs a ref, such as git:main or git:HEAD~1.');
|
|
84
|
+
const git = async (args) => {
|
|
85
|
+
try {
|
|
86
|
+
return (await run('git', args, { cwd: path.dirname(path.resolve(file)), maxBuffer: 16 * 1024 * 1024 })).stdout;
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
const stderr = String(error.stderr ?? '').trim();
|
|
90
|
+
throw new Error(stderr || error.message);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
try {
|
|
94
|
+
await git(['rev-parse', '--verify', '--quiet', `${ref}^{commit}`]);
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
throw new Error(`git has no commit ${ref} where ${file} is: ${error.message || 'unknown ref'}`);
|
|
98
|
+
}
|
|
99
|
+
const object = `${ref}:./${path.basename(file)}`;
|
|
100
|
+
try {
|
|
101
|
+
await git(['cat-file', '-e', object]);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
return git(['show', object]);
|
|
107
|
+
}
|
|
108
|
+
function describe(policy) {
|
|
109
|
+
return `Policy ${policy.name}@${policy.version} (sha256 ${policy.hash.slice(0, 12)})`;
|
|
110
|
+
}
|
|
111
|
+
function report(json, value, text) {
|
|
112
|
+
if (json)
|
|
113
|
+
writeJson(value);
|
|
114
|
+
else
|
|
115
|
+
console.log(text);
|
|
116
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import
|
|
2
|
+
import { readdir, readFile, stat } from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import process from 'node:process';
|
|
4
5
|
import { pathToFileURL } from 'node:url';
|
|
5
|
-
import {
|
|
6
|
-
import { EvalRunner } from './evals/runner.js';
|
|
6
|
+
import { CliUsageError, flagBool, isRecord, numberFlag, parseArgs, printTable, stringFlag, writeJson, } from './cli/args.js';
|
|
7
7
|
import { NexusAI } from './core/nexus.js';
|
|
8
|
-
import {
|
|
8
|
+
import { EvalRunner } from './evals/runner.js';
|
|
9
|
+
import { getModelCapabilities, listKnownModels, listModelsForProvider } from './models/registry.js';
|
|
10
|
+
import { TokenOptimizer } from './optimizer/index.js';
|
|
9
11
|
import { SecurityPipeline } from './security/index.js';
|
|
10
12
|
import { redactSensitiveText } from './security/output-guard.js';
|
|
11
13
|
import { UploadScanner } from './security/upload-scanner.js';
|
|
12
|
-
import { TokenOptimizer } from './optimizer/index.js';
|
|
13
|
-
import { CliUsageError, flagBool, isRecord, numberFlag, parseArgs, printTable, stringFlag, writeJson, } from './cli/args.js';
|
|
14
14
|
const DEFAULT_SCAN_MAX_BYTES = 1_000_000;
|
|
15
15
|
const IGNORED_DIRS = new Set(['.git', 'node_modules', 'dist', 'coverage', '.next', '.turbo']);
|
|
16
16
|
async function main() {
|
|
@@ -78,6 +78,12 @@ async function main() {
|
|
|
78
78
|
await runDeployCommand(subcommand, { positionals: args, flags: parsed.flags });
|
|
79
79
|
return;
|
|
80
80
|
}
|
|
81
|
+
case 'policy': {
|
|
82
|
+
const [subcommand, ...args] = parsed.positionals;
|
|
83
|
+
const { runPolicyCommand } = await import('./cli/policy.js');
|
|
84
|
+
await runPolicyCommand(subcommand, { positionals: args, flags: parsed.flags });
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
81
87
|
case 'optimize':
|
|
82
88
|
await runOptimize(parsed);
|
|
83
89
|
return;
|
|
@@ -431,6 +437,7 @@ Usage:
|
|
|
431
437
|
nexus deploy canary <assistant> <revision> <percent> --url <server> [--reason r] [--version n]
|
|
432
438
|
nexus deploy promote <assistant> <revision> --url <server> [--reason r] [--version n]
|
|
433
439
|
nexus deploy rollback <assistant> [--to revision] --url <server> [--reason r] [--version n]
|
|
440
|
+
nexus policy check <policy.json> [--base <policy.json>|git:<ref>] [--accept path,path] [--json]
|
|
434
441
|
nexus optimize [request.json|prompt.txt] [--model gpt-5-mini] [--max-input-tokens 4000] [--densify] [--json]
|
|
435
442
|
|
|
436
443
|
Eval files can export { cases, client } or { cases, config }. JSON cases may use "expected", "contains", and "match": "includes" | "exact" | "regex".
|
|
@@ -442,7 +449,8 @@ Database modules for "db status" and "db migrate" export { client }, and optiona
|
|
|
442
449
|
"doctor" exits 1 when a check fails, or with --strict when one warns. Its module exports any of { database, client, redis, operations, ai, deployment, close }.
|
|
443
450
|
"deploy" reads NEXUS_SERVER_URL and NEXUS_SERVER_TOKEN when --url and --token are not given.
|
|
444
451
|
"migrate" moves 1.x root imports to their 2.0 subpaths and reports removed options; --check exits 1 while any remain.
|
|
445
|
-
"graph lint" reads a compiled graph's shape and exits 1 on an error finding, or on a warning with --strict
|
|
452
|
+
"graph lint" reads a compiled graph's shape and exits 1 on an error finding, or on a warning with --strict.
|
|
453
|
+
"policy check" exits 1 when the policy is not valid, or, against --base, when a change that widens what a tenant may reach is not acknowledged.`);
|
|
446
454
|
}
|
|
447
455
|
main().catch((error) => {
|
|
448
456
|
const message = error instanceof Error ? error.message : String(error);
|
package/dist/core/lifecycle.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import type { RateLimitConfig } from '../types/config.js';
|
|
2
|
-
import type { ProviderHealthOutcome, LifecycleConfig, LifecycleStage, OperationDescriptor, OperationLifecycleLike, OperationResultInfo, OperationStartOptions, OperationTicket, ProviderCallContext } from '../types/lifecycle.js';
|
|
3
1
|
import type { AuditLogger } from '../ops/audit-logger.js';
|
|
4
2
|
import type { MetricsCollector } from '../ops/metrics.js';
|
|
5
3
|
import { type RateLimiter } from '../ops/rate-limiter.js';
|
|
6
|
-
import type {
|
|
4
|
+
import type { RateLimitConfig } from '../types/config.js';
|
|
5
|
+
import type { ContinuedCallContext, LifecycleConfig, LifecycleStage, OperationDescriptor, OperationLifecycleLike, OperationResultInfo, OperationStartOptions, OperationTicket, ProviderCallContext, ProviderGate, ProviderHealthOutcome } from '../types/lifecycle.js';
|
|
6
|
+
import type { CallResidency, ResidencyDecision, ResidencyPolicy } from '../types/residency.js';
|
|
7
7
|
/** Every lifecycle stage, in the order an operation passes through them. */
|
|
8
8
|
export declare const LIFECYCLE_STAGES: readonly LifecycleStage[];
|
|
9
9
|
/** Raised when `lifecycle.authorize` refuses an operation, before any provider is called. */
|
|
@@ -105,6 +105,21 @@ export declare class OperationLifecycle implements OperationLifecycleLike {
|
|
|
105
105
|
}, plan: OperationPlan<T>, options?: OperationStartOptions & {
|
|
106
106
|
audit?: Record<string, unknown>;
|
|
107
107
|
}): Promise<T>;
|
|
108
|
+
/**
|
|
109
|
+
* What a provider call that continues an operation admitted earlier is given, such as a video job
|
|
110
|
+
* polled after a restart or a batch collected by another worker: its tenant's credentials, resolved
|
|
111
|
+
* as `start()` resolves them, its residency, and a `scrub()` that keeps them out of its errors.
|
|
112
|
+
* Nothing is authorized, budgeted, or audited: the operation it continues was.
|
|
113
|
+
*/
|
|
114
|
+
callContext(input: Omit<OperationDescriptor, 'requestId'> & {
|
|
115
|
+
requestId?: string;
|
|
116
|
+
}): Promise<ContinuedCallContext>;
|
|
117
|
+
/**
|
|
118
|
+
* Which providers and models an operation may reach, under the residency policy and the provider
|
|
119
|
+
* gate, or `undefined` when there is neither: what `start()` gives each operation's context, for
|
|
120
|
+
* code that decides outside one.
|
|
121
|
+
*/
|
|
122
|
+
access(descriptor: Pick<OperationDescriptor, 'tenantId'> & Partial<OperationDescriptor>): Promise<CallResidency | undefined>;
|
|
108
123
|
/**
|
|
109
124
|
* Reports how a provider answered a call a family made itself, such as a realtime connection, to the
|
|
110
125
|
* client's provider health and circuit breaker.
|
|
@@ -117,3 +132,8 @@ export declare class OperationLifecycle implements OperationLifecycleLike {
|
|
|
117
132
|
}
|
|
118
133
|
/** An operation's residency, as the families that call providers check it. */
|
|
119
134
|
export declare function callResidency(policy: ResidencyPolicy, decision: CallResidency['decision']): CallResidency;
|
|
135
|
+
/**
|
|
136
|
+
* What an operation may reach: the providers its residency policy allows, and among them those the
|
|
137
|
+
* provider gate admits, model by model.
|
|
138
|
+
*/
|
|
139
|
+
export declare function callAccess(descriptor: OperationDescriptor | undefined, policy: ResidencyPolicy | undefined, decision: ResidencyDecision | undefined, gate: ProviderGate | undefined): CallResidency;
|
package/dist/core/lifecycle.js
CHANGED
|
@@ -41,7 +41,12 @@ export class OperationLifecycle {
|
|
|
41
41
|
this.runtime = runtime;
|
|
42
42
|
}
|
|
43
43
|
async start(input, options = {}) {
|
|
44
|
-
const
|
|
44
|
+
const policyReference = this.runtime.config?.policy;
|
|
45
|
+
const descriptor = {
|
|
46
|
+
...input,
|
|
47
|
+
requestId: input.requestId?.trim() || generateRequestId(),
|
|
48
|
+
...(policyReference && !input.policy ? { policy: policyReference } : {}),
|
|
49
|
+
};
|
|
45
50
|
const startedAt = Date.now();
|
|
46
51
|
const timeoutMs = options.timeoutMs ?? this.runtime.timeoutMs;
|
|
47
52
|
const deadline = timeoutMs !== undefined && Number.isFinite(timeoutMs) && timeoutMs > 0 ? startedAt + timeoutMs : undefined;
|
|
@@ -64,11 +69,11 @@ export class OperationLifecycle {
|
|
|
64
69
|
await this.runtime.metrics?.recordRequest(labelsOf(descriptor));
|
|
65
70
|
await this.checkRateLimit(descriptor);
|
|
66
71
|
await this.authorize(descriptor);
|
|
67
|
-
const
|
|
68
|
-
if (
|
|
69
|
-
context.residency =
|
|
72
|
+
const access = await this.access(descriptor);
|
|
73
|
+
if (access) {
|
|
74
|
+
context.residency = access;
|
|
70
75
|
if (descriptor.provider)
|
|
71
|
-
|
|
76
|
+
access.check(descriptor.provider, descriptor.model);
|
|
72
77
|
}
|
|
73
78
|
}
|
|
74
79
|
catch (error) {
|
|
@@ -87,7 +92,7 @@ export class OperationLifecycle {
|
|
|
87
92
|
await plan.resolveAssets?.(ticket.context);
|
|
88
93
|
const routed = await plan.route?.();
|
|
89
94
|
if (routed?.provider) {
|
|
90
|
-
ticket.context.residency?.check(routed.provider);
|
|
95
|
+
ticket.context.residency?.check(routed.provider, routed.model ?? ticket.descriptor.model);
|
|
91
96
|
ticket.descriptor.provider = routed.provider;
|
|
92
97
|
}
|
|
93
98
|
if (routed?.model)
|
|
@@ -106,6 +111,26 @@ export class OperationLifecycle {
|
|
|
106
111
|
throw error;
|
|
107
112
|
}
|
|
108
113
|
}
|
|
114
|
+
async callContext(input) {
|
|
115
|
+
const descriptor = { ...input, requestId: input.requestId?.trim() || generateRequestId() };
|
|
116
|
+
const credentials = this.runtime.config?.credentials
|
|
117
|
+
? new CallCredentials(this.runtime.config.credentials, descriptor)
|
|
118
|
+
: undefined;
|
|
119
|
+
const residency = await this.access(descriptor);
|
|
120
|
+
return {
|
|
121
|
+
...(descriptor.tenantId !== undefined ? { tenantId: descriptor.tenantId } : {}),
|
|
122
|
+
...(credentials ? { credential: (provider) => credentials.resolve(provider) } : {}),
|
|
123
|
+
...(residency ? { residency } : {}),
|
|
124
|
+
scrub: (error) => (credentials ? scrubSecrets(error, credentials.secrets) : error),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
async access(descriptor) {
|
|
128
|
+
const policy = this.runtime.config?.residency;
|
|
129
|
+
const gate = this.runtime.config?.providers;
|
|
130
|
+
if (!policy && !gate)
|
|
131
|
+
return undefined;
|
|
132
|
+
return callAccess(descriptor, policy, policy ? await policy.regionsFor(descriptor.tenantId) : undefined, gate);
|
|
133
|
+
}
|
|
109
134
|
reportHealth(provider, outcome) {
|
|
110
135
|
const health = this.runtime.providerHealth;
|
|
111
136
|
if (!health || !provider)
|
|
@@ -124,7 +149,9 @@ export class OperationLifecycle {
|
|
|
124
149
|
!config?.budget &&
|
|
125
150
|
!config?.hooks &&
|
|
126
151
|
!config?.credentials &&
|
|
127
|
-
!config?.residency
|
|
152
|
+
!config?.residency &&
|
|
153
|
+
!config?.providers &&
|
|
154
|
+
!config?.policy);
|
|
128
155
|
}
|
|
129
156
|
async checkRateLimit(descriptor) {
|
|
130
157
|
const { rateLimiter, rateLimit } = this.runtime;
|
|
@@ -160,13 +187,48 @@ export class OperationLifecycle {
|
|
|
160
187
|
}
|
|
161
188
|
}
|
|
162
189
|
export function callResidency(policy, decision) {
|
|
190
|
+
return callAccess(undefined, policy, decision, undefined);
|
|
191
|
+
}
|
|
192
|
+
export function callAccess(descriptor, policy, decision, gate) {
|
|
193
|
+
const resolved = decision ?? { rule: 'any region' };
|
|
194
|
+
const admits = (provider, model) => !gate || !descriptor || gate.check(descriptor, provider, model) === true;
|
|
195
|
+
const resident = (provider) => !policy || policy.allows(policy.regionOf(provider), resolved);
|
|
196
|
+
const checkResidency = (provider) => {
|
|
197
|
+
if (!policy)
|
|
198
|
+
return;
|
|
199
|
+
const region = policy.regionOf(provider);
|
|
200
|
+
policy.check({ kind: 'provider', name: provider, ...(region ? { region } : {}) }, resolved);
|
|
201
|
+
};
|
|
202
|
+
const checkGate = (provider, model) => {
|
|
203
|
+
if (!gate || !descriptor)
|
|
204
|
+
return;
|
|
205
|
+
const verdict = gate.check(descriptor, provider, model);
|
|
206
|
+
if (verdict !== true)
|
|
207
|
+
throw new OperationDeniedError(descriptor, verdict);
|
|
208
|
+
};
|
|
163
209
|
return {
|
|
164
|
-
decision,
|
|
165
|
-
allows: (provider) =>
|
|
166
|
-
place: (provider) =>
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
210
|
+
decision: resolved,
|
|
211
|
+
allows: (provider, model) => resident(provider) && admits(provider, model),
|
|
212
|
+
place: (provider, model) => {
|
|
213
|
+
const placed = policy ? policy.place(provider, resolved) : { provider };
|
|
214
|
+
return placed && admits(placed.provider, model) ? placed : undefined;
|
|
215
|
+
},
|
|
216
|
+
check: (provider, model) => {
|
|
217
|
+
checkResidency(provider);
|
|
218
|
+
checkGate(provider, model);
|
|
219
|
+
},
|
|
220
|
+
refusal: (provider, model) => {
|
|
221
|
+
try {
|
|
222
|
+
const placed = policy ? policy.place(provider, resolved) : { provider };
|
|
223
|
+
if (!placed)
|
|
224
|
+
checkResidency(provider);
|
|
225
|
+
else
|
|
226
|
+
checkGate(placed.provider, model);
|
|
227
|
+
return undefined;
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
return error;
|
|
231
|
+
}
|
|
170
232
|
},
|
|
171
233
|
};
|
|
172
234
|
}
|
|
@@ -320,6 +382,7 @@ class Ticket {
|
|
|
320
382
|
model: this.descriptor.model,
|
|
321
383
|
provider: this.descriptor.provider,
|
|
322
384
|
timestamp: new Date().toISOString(),
|
|
385
|
+
...(this.descriptor.policy ? { policy: this.descriptor.policy } : {}),
|
|
323
386
|
...(this.descriptor.metadata || metadata ? { metadata: { ...this.descriptor.metadata, ...metadata } } : {}),
|
|
324
387
|
});
|
|
325
388
|
}
|