@jini-ai/daemon 0.5.2 → 0.5.4
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/CHANGELOG.md +11 -0
- package/dist/agent-executor/contracts.d.ts +26 -20
- package/dist/agent-executor/contracts.js +0 -9
- package/dist/agent-executor/event-translation.d.ts +2 -2
- package/dist/agent-executor/event-translation.js +1 -2
- package/dist/agent-executor/events.d.ts +9 -4
- package/dist/agent-executor/events.js +29 -7
- package/dist/agent-executor/handoff.d.ts +1 -1
- package/dist/agent-executor/handoff.js +0 -1
- package/dist/agent-executor/launch.d.ts +1 -1
- package/dist/agent-executor/launch.js +24 -1
- package/dist/agent-executor/mcp-bridge.d.ts +2 -2
- package/dist/agent-executor/mcp-bridge.js +0 -2
- package/dist/agent-executor/model-receipts.d.ts +6 -0
- package/dist/agent-executor/model-receipts.js +26 -0
- package/dist/agent-executor.d.ts +8 -1663
- package/dist/agent-executor.js +8 -3005
- package/dist/attachment-content.d.ts +45 -0
- package/dist/attachment-content.js +40 -0
- package/dist/core/read-only-tools.d.ts +130 -0
- package/dist/core/read-only-tools.js +115 -0
- package/dist/core/tool-audit.d.ts +131 -0
- package/dist/core/tool-audit.js +113 -0
- package/dist/http/active-context.d.ts +2 -2
- package/dist/http/agents.d.ts +8 -2
- package/dist/http/agents.js +6 -2
- package/dist/http/attachments.d.ts +16 -7
- package/dist/http/attachments.js +3 -2
- package/dist/http/component-catalog.d.ts +2 -2
- package/dist/http/connectors.d.ts +17 -17
- package/dist/http/daemon-status.d.ts +2 -2
- package/dist/http/db-ops.d.ts +3 -3
- package/dist/http/delegated-tools.d.ts +1 -1
- package/dist/http/delegated-tools.js +1 -1
- package/dist/http/frontend-sessions.d.ts +1 -1
- package/dist/http/health.d.ts +6 -6
- package/dist/http/host-tools.d.ts +2 -2
- package/dist/http/media.d.ts +4 -4
- package/dist/http/memory.d.ts +15 -15
- package/dist/http/remote-run-events.d.ts +2 -2
- package/dist/http/research.d.ts +1 -1
- package/dist/http/routines.d.ts +7 -7
- package/dist/http/run-credentials.d.ts +2 -2
- package/dist/http/run-credentials.js +2 -2
- package/dist/http/runs.d.ts +4 -4
- package/dist/http/terminals.d.ts +6 -6
- package/dist/http/tool-catalog.d.ts +2 -2
- package/dist/http/xai.d.ts +6 -6
- package/dist/index.d.ts +76 -571
- package/dist/index.js +76 -571
- package/dist/read-only-tools.d.ts +1 -129
- package/dist/read-only-tools.js +2 -114
- package/dist/run-credentials.d.ts +47 -23
- package/dist/run-credentials.js +24 -8
- package/dist/run-lifecycle.d.ts +1 -1
- package/dist/run-lifecycle.js +6 -5
- package/dist/scheduler.d.ts +5 -0
- package/dist/scheduler.js +13 -1
- package/dist/session-coordination.d.ts +58 -24
- package/dist/session-coordination.js +40 -12
- package/dist/store/event-log/sqlite.js +3 -3
- package/dist/surface-exchanges.d.ts +207 -61
- package/dist/surface-exchanges.js +167 -20
- package/dist/tool-audit.d.ts +1 -113
- package/dist/tool-audit.js +2 -94
- package/dist/tool-recovery/diagnostics.d.ts +132 -0
- package/dist/tool-recovery/diagnostics.js +110 -0
- package/dist/tool-recovery/federated-refusal.d.ts +92 -0
- package/dist/tool-recovery/federated-refusal.js +152 -0
- package/dist/tool-recovery/index.d.ts +6 -0
- package/dist/tool-recovery/index.js +6 -0
- package/dist/tool-recovery/recovery.d.ts +49 -0
- package/dist/tool-recovery/recovery.js +419 -0
- package/dist/tool-recovery/redaction.d.ts +166 -0
- package/dist/tool-recovery/redaction.js +192 -0
- package/dist/tool-recovery/stack.d.ts +69 -0
- package/dist/tool-recovery/stack.js +62 -0
- package/package.json +35 -12
|
@@ -1,130 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
import type { ToolExecutor } from './tool-executor.js';
|
|
3
|
-
import type { IdGenerator } from '@jini-ai/core/primitives';
|
|
4
|
-
/**
|
|
5
|
-
* A `Principal` narrowed to tools that only read.
|
|
6
|
-
*
|
|
7
|
-
* An HTTP route preflight only ever validates the OUTER `toolId` — the one the caller
|
|
8
|
-
* named in the HTTP request. It cannot see what a consumer's own `ToolExecutor` composition does
|
|
9
|
-
* once `bridge.execute()` hands control to `deps.toolExecutor`: a decorator that composition
|
|
10
|
-
* stacks on top of the bare executor (a recovery loop that retries through a different tool id
|
|
11
|
-
* after a failure, for instance) can dispatch a tool the caller never named, using the exact same
|
|
12
|
-
* `principal` the outer call carried — one hop past the only check the HTTP route ever ran. Adding a
|
|
13
|
-
* second request-shaped flag would not close that gap either: it would have to be threaded, by
|
|
14
|
-
* hand, through every decorator between here and the handler, and a decorator that forgot would
|
|
15
|
-
* fail open silently. `principal` is the one value every layer already passes unchanged, so the
|
|
16
|
-
* constraint travels as an ATTENUATED IDENTITY instead — a principal permitted to invoke only
|
|
17
|
-
* read-only tools, checkable by {@link refuseNonReadOnlyDispatch} at any depth a consumer's own
|
|
18
|
-
* composition cares to ask.
|
|
19
|
-
*
|
|
20
|
-
* A distinct field rather than a `Principal.roles` entry: `roles` is what a `ToolPolicy` branches
|
|
21
|
-
* on, and a synthetic role would silently join that decision for every registration in the
|
|
22
|
-
* process. This field is inert to everything except {@link refuseNonReadOnlyDispatch}.
|
|
23
|
-
*/
|
|
24
|
-
export interface ReadOnlyConstrainedPrincipal extends Principal {
|
|
25
|
-
readonly toolAccess: 'read-only';
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* Default daemon refusal wording, replaceable by the host. Unknown registrations and writes share
|
|
29
|
-
* one refusal: neither corroborates a safe read, and catalog discovery already reveals existence.
|
|
30
|
-
* Missing verification metadata always refuses, naming the missing wiring so the host can fix it.
|
|
31
|
-
*/
|
|
32
|
-
export declare const defaultDaemonMessages: {
|
|
33
|
-
readOnly: {
|
|
34
|
-
unverifiableMessage: string;
|
|
35
|
-
toolRefusalMessage: ({ toolId }: {
|
|
36
|
-
toolId: string;
|
|
37
|
-
}) => string;
|
|
38
|
-
};
|
|
39
|
-
};
|
|
40
|
-
/** Required host wording; absent metadata is always a refusal. */
|
|
41
|
-
export interface ReadOnlyToolMessages {
|
|
42
|
-
readonly unverifiableMessage: string;
|
|
43
|
-
toolRefusalMessage(required: {
|
|
44
|
-
toolId: string;
|
|
45
|
-
}): string;
|
|
46
|
-
}
|
|
47
|
-
export interface ReadOnlyToolCheckRequired {
|
|
48
|
-
readonly toolId: string;
|
|
49
|
-
readonly registry: Pick<ToolRegistry, 'list'> | undefined;
|
|
50
|
-
readonly messages: ReadOnlyToolMessages;
|
|
51
|
-
}
|
|
52
|
-
export interface ReadOnlyToolConstraintRequired {
|
|
53
|
-
readonly inner: ToolExecutor;
|
|
54
|
-
readonly registry: Pick<ToolRegistry, 'list'> | undefined;
|
|
55
|
-
readonly idGenerator: IdGenerator;
|
|
56
|
-
readonly messages: ReadOnlyToolMessages;
|
|
57
|
-
}
|
|
58
|
-
/** @returns A copied, attenuated principal; its roles are unchanged. */
|
|
59
|
-
export declare function constrainPrincipalToReadOnlyTools({ principal }: {
|
|
60
|
-
principal: Principal;
|
|
61
|
-
}, _optional?: Record<string, never>): ReadOnlyConstrainedPrincipal;
|
|
62
|
-
/** @returns Whether attenuation survived the executor/decorator boundary. */
|
|
63
|
-
export declare function principalIsReadOnlyConstrained({ principal }: {
|
|
64
|
-
principal: Principal;
|
|
65
|
-
}, _optional?: Record<string, never>): boolean;
|
|
66
|
-
/** @returns Required host wording for an unknown or non-read-only tool. */
|
|
67
|
-
export declare function readOnlyToolRefusalMessage({ toolId, messages }: {
|
|
68
|
-
toolId: string;
|
|
69
|
-
messages: ReadOnlyToolMessages;
|
|
70
|
-
}, _optional?: Record<string, never>): string;
|
|
71
|
-
/**
|
|
72
|
-
* Single read-only registration decision, shared by routes and inner dispatch.
|
|
73
|
-
* @returns Null for an explicitly read-only registration, otherwise host refusal text.
|
|
74
|
-
*/
|
|
75
|
-
export declare function checkReadOnlyTool({ toolId, registry, messages }: ReadOnlyToolCheckRequired, _optional?: Record<string, never>): string | null;
|
|
76
|
-
/**
|
|
77
|
-
* THE read-only dispatch decision for any tool id a consumer's own `ToolExecutor` composition is
|
|
78
|
-
* about to invoke — the outer call the route already checked, or a nested one a decorator issues
|
|
79
|
-
* one or more hops in. Every consumer's own composition should ask this before dispatching on
|
|
80
|
-
* behalf of a principal it did not itself resolve; none should re-implement the check, so changing
|
|
81
|
-
* what the constraint means stays one edit.
|
|
82
|
-
*
|
|
83
|
-
* @param required.principal - The principal the dispatch would run under. Unconstrained ⇒ always
|
|
84
|
-
* `null` — this is a no-op for every pre-existing caller that never attenuates a principal.
|
|
85
|
-
* @param required.toolId - The id actually about to be dispatched, which is not necessarily the id
|
|
86
|
-
* named in the original request — that difference is the whole reason this function exists.
|
|
87
|
-
* @param required.registry - Descriptors to resolve `toolId` against; `undefined` ⇒ refuse (an
|
|
88
|
-
* unverifiable constrained dispatch is refused, never waived, matching `checkReadOnlyTool`).
|
|
89
|
-
* @returns `null` when the dispatch may proceed, otherwise the refusal text to report.
|
|
90
|
-
* @complexity O(1) for an unconstrained principal; O(n) in registered tool count for a constrained
|
|
91
|
-
* one, since `ToolRegistry` exposes enumeration rather than lookup by id.
|
|
92
|
-
*/
|
|
93
|
-
export declare function refuseNonReadOnlyDispatch(required: ReadOnlyToolCheckRequired & {
|
|
94
|
-
readonly principal: Principal;
|
|
95
|
-
}, _optional?: Record<string, never>): string | null;
|
|
96
|
-
/** @returns Caller-formatted recovery refusal, preserving the original refusal verbatim as input. */
|
|
97
|
-
export declare function readOnlyRemedyRefusalMessage({ refusal, formatMessage }: {
|
|
98
|
-
refusal: string;
|
|
99
|
-
formatMessage: (required: {
|
|
100
|
-
refusal: string;
|
|
101
|
-
}) => string;
|
|
102
|
-
}, _optional?: Record<string, never>): string;
|
|
103
|
-
/**
|
|
104
|
-
* Wraps `inner` so a read-only-constrained principal (one {@link constrainPrincipalToReadOnlyTools}
|
|
105
|
-
* attenuated) cannot dispatch a tool that isn't registered read-only — whichever layer chose the
|
|
106
|
-
* id. This is what turns the attenuation into an actual gate rather than a field nobody reads:
|
|
107
|
-
* {@link refuseNonReadOnlyDispatch} is the decision, this is its enforcement point.
|
|
108
|
-
*
|
|
109
|
-
* **Compose this as close to the bare `ToolExecutor` as your own stack allows** — directly around
|
|
110
|
-
* `@jini-ai/daemon`'s `createToolExecutor` output, beneath any decorator of your own that might
|
|
111
|
-
* dispatch a tool id the caller never named (a retry, a recovery loop that calls a different tool
|
|
112
|
-
* as a remedy, ...). Every such decorator's own `inner.execute` call still has to come through here
|
|
113
|
-
* to reach a handler, which is what makes this a gate on the CLASS of defect (an unnamed nested
|
|
114
|
-
* dispatch) rather than on one instance of it.
|
|
115
|
-
*
|
|
116
|
-
* `delegatedToolExecuteRoute`'s own `handle` additionally composes this around whatever
|
|
117
|
-
* `ToolExecutor` a host supplies, whenever a call attenuates its principal. That outer composition
|
|
118
|
-
* only re-covers the SAME outer `toolId` the route preflight already checked — it
|
|
119
|
-
* cannot see a nested dispatch a host's own inner decorator issues, because that decorator holds a
|
|
120
|
-
* reference to whatever `ToolExecutor` IT was built with, not to the HTTP route's wrapped one. Closing
|
|
121
|
-
* the nested case for a given host's stack requires that host to compose this decorator itself,
|
|
122
|
-
* innermost, in its own `ToolExecutor` construction — this export exists so it can, without having
|
|
123
|
-
* to rediscover or reimplement the mechanism.
|
|
124
|
-
*
|
|
125
|
-
* @returns A drop-in `ToolExecutor`; `resumeConfirmation`/`cancel`/`getAuditRecord` delegate
|
|
126
|
-
* straight through.
|
|
127
|
-
* @complexity One extra property read per unconstrained call; one registry scan per constrained one.
|
|
128
|
-
*/
|
|
129
|
-
export declare function withReadOnlyToolConstraint({ inner, registry, idGenerator, messages }: ReadOnlyToolConstraintRequired, _optional?: Record<string, never>): ToolExecutor;
|
|
1
|
+
export * from './core/read-only-tools.js';
|
|
130
2
|
//# sourceMappingURL=read-only-tools.d.ts.map
|
package/dist/read-only-tools.js
CHANGED
|
@@ -1,115 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
* Default daemon refusal wording, replaceable by the host. Unknown registrations and writes share
|
|
4
|
-
* one refusal: neither corroborates a safe read, and catalog discovery already reveals existence.
|
|
5
|
-
* Missing verification metadata always refuses, naming the missing wiring so the host can fix it.
|
|
6
|
-
*/
|
|
7
|
-
export const defaultDaemonMessages = {
|
|
8
|
-
readOnly: {
|
|
9
|
-
unverifiableMessage: 'this host cannot verify read-only tools — POST /api/delegated-tool-calls was mounted without DelegatedToolsHttpDeps.toolRegistry, so a requireReadOnly call cannot be checked and is refused',
|
|
10
|
-
toolRefusalMessage: ({ toolId }) => `tool "${toolId}" is not registered as read-only — this gateway executes only tools whose registration declares readOnly; call it through execute_delegated_tool instead`,
|
|
11
|
-
},
|
|
12
|
-
};
|
|
13
|
-
/** @returns A copied, attenuated principal; its roles are unchanged. */
|
|
14
|
-
export function constrainPrincipalToReadOnlyTools({ principal }, _optional = {}) {
|
|
15
|
-
return { ...principal, toolAccess: 'read-only' };
|
|
16
|
-
}
|
|
17
|
-
// Read structurally so passing through layers typed as the base Principal cannot erase the
|
|
18
|
-
// execution's authority constraint.
|
|
19
|
-
/** @returns Whether attenuation survived the executor/decorator boundary. */
|
|
20
|
-
export function principalIsReadOnlyConstrained({ principal }, _optional = {}) {
|
|
21
|
-
return 'toolAccess' in principal && principal.toolAccess === 'read-only';
|
|
22
|
-
}
|
|
23
|
-
/** @returns Required host wording for an unknown or non-read-only tool. */
|
|
24
|
-
export function readOnlyToolRefusalMessage({ toolId, messages }, _optional = {}) {
|
|
25
|
-
return messages.toolRefusalMessage({ toolId });
|
|
26
|
-
}
|
|
27
|
-
// Unknown registrations and omitted read-only classification both lack evidence of safety.
|
|
28
|
-
// Every dispatch/preflight consults this decision; recovery preflight prevents asking a human
|
|
29
|
-
// for form input that the innermost gate would refuse after submission.
|
|
30
|
-
/**
|
|
31
|
-
* Single read-only registration decision, shared by routes and inner dispatch.
|
|
32
|
-
* @returns Null for an explicitly read-only registration, otherwise host refusal text.
|
|
33
|
-
*/
|
|
34
|
-
export function checkReadOnlyTool({ toolId, registry, messages }, _optional = {}) {
|
|
35
|
-
if (registry === undefined)
|
|
36
|
-
return messages.unverifiableMessage;
|
|
37
|
-
const descriptor = registry.list({}).find((candidate) => candidate.id === toolId);
|
|
38
|
-
if (isReadOnlyTool({ descriptor }))
|
|
39
|
-
return null;
|
|
40
|
-
return readOnlyToolRefusalMessage({ toolId, messages });
|
|
41
|
-
}
|
|
42
|
-
/**
|
|
43
|
-
* THE read-only dispatch decision for any tool id a consumer's own `ToolExecutor` composition is
|
|
44
|
-
* about to invoke — the outer call the route already checked, or a nested one a decorator issues
|
|
45
|
-
* one or more hops in. Every consumer's own composition should ask this before dispatching on
|
|
46
|
-
* behalf of a principal it did not itself resolve; none should re-implement the check, so changing
|
|
47
|
-
* what the constraint means stays one edit.
|
|
48
|
-
*
|
|
49
|
-
* @param required.principal - The principal the dispatch would run under. Unconstrained ⇒ always
|
|
50
|
-
* `null` — this is a no-op for every pre-existing caller that never attenuates a principal.
|
|
51
|
-
* @param required.toolId - The id actually about to be dispatched, which is not necessarily the id
|
|
52
|
-
* named in the original request — that difference is the whole reason this function exists.
|
|
53
|
-
* @param required.registry - Descriptors to resolve `toolId` against; `undefined` ⇒ refuse (an
|
|
54
|
-
* unverifiable constrained dispatch is refused, never waived, matching `checkReadOnlyTool`).
|
|
55
|
-
* @returns `null` when the dispatch may proceed, otherwise the refusal text to report.
|
|
56
|
-
* @complexity O(1) for an unconstrained principal; O(n) in registered tool count for a constrained
|
|
57
|
-
* one, since `ToolRegistry` exposes enumeration rather than lookup by id.
|
|
58
|
-
*/
|
|
59
|
-
export function refuseNonReadOnlyDispatch(required, _optional = {}) {
|
|
60
|
-
if (!principalIsReadOnlyConstrained({ principal: required.principal }))
|
|
61
|
-
return null;
|
|
62
|
-
return checkReadOnlyTool(required);
|
|
63
|
-
}
|
|
64
|
-
// Report the original failure alongside the refused remedy; silently hiding an automatic
|
|
65
|
-
// recovery attempt would mislead the caller even when the constraint correctly prevents writes.
|
|
66
|
-
/** @returns Caller-formatted recovery refusal, preserving the original refusal verbatim as input. */
|
|
67
|
-
export function readOnlyRemedyRefusalMessage({ refusal, formatMessage }, _optional = {}) {
|
|
68
|
-
return formatMessage({ refusal });
|
|
69
|
-
}
|
|
70
|
-
// A route can validate the caller's read-only tool and still miss a recovery decorator choosing
|
|
71
|
-
// a writing remedy afterward. Enforce on every actual dispatch at the innermost executor to
|
|
72
|
-
// close that class of authority laundering, including dispatchers introduced later.
|
|
73
|
-
/**
|
|
74
|
-
* Wraps `inner` so a read-only-constrained principal (one {@link constrainPrincipalToReadOnlyTools}
|
|
75
|
-
* attenuated) cannot dispatch a tool that isn't registered read-only — whichever layer chose the
|
|
76
|
-
* id. This is what turns the attenuation into an actual gate rather than a field nobody reads:
|
|
77
|
-
* {@link refuseNonReadOnlyDispatch} is the decision, this is its enforcement point.
|
|
78
|
-
*
|
|
79
|
-
* **Compose this as close to the bare `ToolExecutor` as your own stack allows** — directly around
|
|
80
|
-
* `@jini-ai/daemon`'s `createToolExecutor` output, beneath any decorator of your own that might
|
|
81
|
-
* dispatch a tool id the caller never named (a retry, a recovery loop that calls a different tool
|
|
82
|
-
* as a remedy, ...). Every such decorator's own `inner.execute` call still has to come through here
|
|
83
|
-
* to reach a handler, which is what makes this a gate on the CLASS of defect (an unnamed nested
|
|
84
|
-
* dispatch) rather than on one instance of it.
|
|
85
|
-
*
|
|
86
|
-
* `delegatedToolExecuteRoute`'s own `handle` additionally composes this around whatever
|
|
87
|
-
* `ToolExecutor` a host supplies, whenever a call attenuates its principal. That outer composition
|
|
88
|
-
* only re-covers the SAME outer `toolId` the route preflight already checked — it
|
|
89
|
-
* cannot see a nested dispatch a host's own inner decorator issues, because that decorator holds a
|
|
90
|
-
* reference to whatever `ToolExecutor` IT was built with, not to the HTTP route's wrapped one. Closing
|
|
91
|
-
* the nested case for a given host's stack requires that host to compose this decorator itself,
|
|
92
|
-
* innermost, in its own `ToolExecutor` construction — this export exists so it can, without having
|
|
93
|
-
* to rediscover or reimplement the mechanism.
|
|
94
|
-
*
|
|
95
|
-
* @returns A drop-in `ToolExecutor`; `resumeConfirmation`/`cancel`/`getAuditRecord` delegate
|
|
96
|
-
* straight through.
|
|
97
|
-
* @complexity One extra property read per unconstrained call; one registry scan per constrained one.
|
|
98
|
-
*/
|
|
99
|
-
export function withReadOnlyToolConstraint({ inner, registry, idGenerator, messages }, _optional = {}) {
|
|
100
|
-
return {
|
|
101
|
-
execute: async (requiredArgs, optionalArgs = {}) => {
|
|
102
|
-
const { principal, toolId } = requiredArgs;
|
|
103
|
-
const refusal = refuseNonReadOnlyDispatch({ principal, toolId, registry, messages });
|
|
104
|
-
// Deny before inner: no handler or kernel audit record exists. Use the authorization result
|
|
105
|
-
// union instead of throwing, and mint a denial ID because there is no inner execution ID.
|
|
106
|
-
if (refusal !== null)
|
|
107
|
-
return { executionId: idGenerator.newId(), status: 'denied', error: refusal };
|
|
108
|
-
return inner.execute(requiredArgs, optionalArgs);
|
|
109
|
-
},
|
|
110
|
-
resumeConfirmation: (requiredArgs) => inner.resumeConfirmation(requiredArgs),
|
|
111
|
-
cancel: (requiredArgs) => inner.cancel(requiredArgs),
|
|
112
|
-
getAuditRecord: (requiredArgs) => inner.getAuditRecord(requiredArgs),
|
|
113
|
-
};
|
|
114
|
-
}
|
|
1
|
+
// Compatibility path; the shared implementation lives in core.
|
|
2
|
+
export * from './core/read-only-tools.js';
|
|
115
3
|
//# sourceMappingURL=read-only-tools.js.map
|
|
@@ -3,60 +3,70 @@ export interface RunScopedCaller {
|
|
|
3
3
|
readonly principalId: string;
|
|
4
4
|
}
|
|
5
5
|
export interface CredentialCryptoPort {
|
|
6
|
-
mintToken(args: Record<string, never>): string;
|
|
6
|
+
mintToken(args: Record<string, never>, optional?: Record<string, never>): string;
|
|
7
7
|
digest(args: {
|
|
8
8
|
token: string;
|
|
9
|
-
}): string;
|
|
9
|
+
}, optional?: Record<string, never>): string;
|
|
10
10
|
tokensMatch(args: {
|
|
11
11
|
presented: string;
|
|
12
12
|
expected: string;
|
|
13
|
-
}): boolean;
|
|
13
|
+
}, optional?: Record<string, never>): boolean;
|
|
14
14
|
}
|
|
15
|
-
/** Explicit Node adapter
|
|
16
|
-
|
|
15
|
+
/** Explicit Node crypto adapter: 32 random bytes produce a 64-hex-character bearer.
|
|
16
|
+
* Length rejection reveals only that fixed public length; content matching is timing-safe.
|
|
17
|
+
* Consumers can inject an alternate implementation. */
|
|
18
|
+
export declare function createNodeCredentialCrypto(_args: Record<string, never>, _optional?: Record<string, never>): CredentialCryptoPort;
|
|
17
19
|
export interface RunScopedCallerResolver {
|
|
18
20
|
resolveCaller(args: {
|
|
19
21
|
token: string;
|
|
20
|
-
}): RunScopedCaller | undefined;
|
|
22
|
+
}, optional?: Record<string, never>): RunScopedCaller | undefined;
|
|
21
23
|
}
|
|
24
|
+
/** A bridge receives a per-run credential, never the proxy boot token: that token would let it
|
|
25
|
+
* assert another principal or start unrelated runs. Resolve identity server-side from the live
|
|
26
|
+
* principal on every call; terminal liveness revokes access even if explicit cleanup fails.
|
|
27
|
+
* Lookup keys are SHA-256 digests rather than raw bearer secrets. Restart drops the local store. */
|
|
22
28
|
export interface RunScopedCredentials extends RunScopedCallerResolver {
|
|
29
|
+
/** Idempotent for a live run; refuses unknown/ended runs and empty or colliding tokens. */
|
|
23
30
|
mint(args: {
|
|
24
31
|
runId: string;
|
|
25
|
-
}): string;
|
|
32
|
+
}, optional?: Record<string, never>): string;
|
|
26
33
|
resolvePrincipal(args: {
|
|
27
34
|
token: string;
|
|
28
|
-
}): string | undefined;
|
|
35
|
+
}, optional?: Record<string, never>): string | undefined;
|
|
36
|
+
/** Idempotent cleanup to prevent map growth; liveness is checked independently. */
|
|
29
37
|
revoke(args: {
|
|
30
38
|
runId: string;
|
|
31
|
-
}): void;
|
|
39
|
+
}, optional?: Record<string, never>): void;
|
|
32
40
|
}
|
|
33
41
|
export declare function createRunScopedCredentials(deps: {
|
|
34
42
|
principalOfLiveRun(args: {
|
|
35
43
|
runId: string;
|
|
36
|
-
}): string | undefined;
|
|
44
|
+
}, optional?: Record<string, never>): string | undefined;
|
|
37
45
|
crypto: Pick<CredentialCryptoPort, 'mintToken' | 'digest'>;
|
|
38
|
-
}): RunScopedCredentials;
|
|
46
|
+
}, _optional?: Record<string, never>): RunScopedCredentials;
|
|
47
|
+
/** Preserve an operator token or mint one before spawning a child, which inherits env at spawn.
|
|
48
|
+
* Repeated calls must not rotate the value and invalidate a child that already received it. */
|
|
39
49
|
export declare function ensureAgentDaemonToken({ env, envVarName, crypto }: {
|
|
40
50
|
env: Record<string, string | undefined>;
|
|
41
51
|
envVarName: string;
|
|
42
52
|
crypto: Pick<CredentialCryptoPort, 'mintToken'>;
|
|
43
|
-
}): string;
|
|
53
|
+
}, _optional?: Record<string, never>): string;
|
|
44
54
|
export interface RouteAccessPolicy {
|
|
45
55
|
/** Return a URL-decoded run ID; malformed encoding must throw URIError. */
|
|
46
56
|
targetRun(args: {
|
|
47
57
|
path: string;
|
|
48
|
-
}): string | undefined;
|
|
58
|
+
}, optional?: Record<string, never>): string | undefined;
|
|
49
59
|
allowsRunScoped(args: {
|
|
50
60
|
method: string;
|
|
51
61
|
path: string;
|
|
52
|
-
}): boolean;
|
|
62
|
+
}, optional?: Record<string, never>): boolean;
|
|
53
63
|
isDelegatedCall(args: {
|
|
54
64
|
method: string;
|
|
55
65
|
path: string;
|
|
56
|
-
}): boolean;
|
|
66
|
+
}, optional?: Record<string, never>): boolean;
|
|
57
67
|
isEventStream(args: {
|
|
58
68
|
path: string;
|
|
59
|
-
}): boolean;
|
|
69
|
+
}, optional?: Record<string, never>): boolean;
|
|
60
70
|
}
|
|
61
71
|
/** Host supplies route names; this module owns matching and run isolation. */
|
|
62
72
|
export declare function createRouteAccessPolicy({ runPathPrefix, delegatedToolCallsPath, eventStreamSuffix, allowedRoutes }: {
|
|
@@ -67,7 +77,7 @@ export declare function createRouteAccessPolicy({ runPathPrefix, delegatedToolCa
|
|
|
67
77
|
method: string;
|
|
68
78
|
path: RegExp;
|
|
69
79
|
}[];
|
|
70
|
-
}): RouteAccessPolicy;
|
|
80
|
+
}, _optional?: Record<string, never>): RouteAccessPolicy;
|
|
71
81
|
export type AccessDecision = {
|
|
72
82
|
allowed: true;
|
|
73
83
|
principalHeader?: {
|
|
@@ -80,6 +90,12 @@ export type AccessDecision = {
|
|
|
80
90
|
body: unknown;
|
|
81
91
|
};
|
|
82
92
|
/** Transport-independent bearer gate. Apply principalHeader only after an allowed decision. */
|
|
93
|
+
/** Loopback is not authentication: another local process can reach the daemon. No peer-address
|
|
94
|
+
* exemption is allowed. Missing configuration refuses service (503); bad credentials receive 401.
|
|
95
|
+
* Mount authentication before body parsing and ownership checks. Explicit path exemptions use
|
|
96
|
+
* exact equality and apply only without a credential; presenting one always opts into validation.
|
|
97
|
+
* A run bearer is bound to its run before route allowlisting and overwrites caller identity.
|
|
98
|
+
* Mount the delegated-body gate after parsing so body.runId must also match that credential. */
|
|
83
99
|
export declare function authorizeDaemonRequest({ request, env, envVarName, principalHeaderName, authorizationHeaderName, crypto, routes }: {
|
|
84
100
|
request: {
|
|
85
101
|
method: string;
|
|
@@ -98,20 +114,26 @@ export declare function authorizeDaemonRequest({ request, env, envVarName, princ
|
|
|
98
114
|
runScopedCallers?: RunScopedCallerResolver;
|
|
99
115
|
validateDelegatedRunId?: boolean;
|
|
100
116
|
}): AccessDecision;
|
|
117
|
+
/** Ownership survives terminal transition until the run record expires; live-principal mappings
|
|
118
|
+
* instead end with execution. Keep ownership in the run-owning process so lifetimes cannot drift. */
|
|
101
119
|
export interface RunOwnerRegistry {
|
|
102
120
|
record(args: {
|
|
103
121
|
runId: string;
|
|
104
122
|
principalId: string;
|
|
105
|
-
}): void;
|
|
123
|
+
}, optional?: Record<string, never>): void;
|
|
106
124
|
ownerOf(args: {
|
|
107
125
|
runId: string;
|
|
108
|
-
}): string | undefined;
|
|
126
|
+
}, optional?: Record<string, never>): string | undefined;
|
|
109
127
|
forget(args: {
|
|
110
128
|
runId: string;
|
|
111
|
-
}): void;
|
|
129
|
+
}, optional?: Record<string, never>): void;
|
|
112
130
|
}
|
|
113
|
-
export declare function createRunOwnerRegistry(_args: Record<string, never>): RunOwnerRegistry;
|
|
131
|
+
export declare function createRunOwnerRegistry(_args: Record<string, never>, _optional?: Record<string, never>): RunOwnerRegistry;
|
|
114
132
|
/** Mount after the bearer gate; principalId must be server verified. */
|
|
133
|
+
/** Trust principal headers only downstream of the bearer gate. Missing identity fails closed.
|
|
134
|
+
* Non-owners receive the exact unknown-run response (including the JSON/SSE distinction), so
|
|
135
|
+
* refusal cannot confirm existence. An existing unowned run or failed lookup also denies; only
|
|
136
|
+
* a genuinely absent run passes to the route for its own 404. */
|
|
115
137
|
export declare function authorizeRunOwnership({ runId, principalId, principalHeaderName, registry, runExists, eventStream }: {
|
|
116
138
|
runId: string | undefined;
|
|
117
139
|
principalId: string | undefined;
|
|
@@ -121,7 +143,9 @@ export declare function authorizeRunOwnership({ runId, principalId, principalHea
|
|
|
121
143
|
runId: string;
|
|
122
144
|
}) => Promise<boolean>;
|
|
123
145
|
eventStream: boolean;
|
|
124
|
-
}): Promise<AccessDecision>;
|
|
146
|
+
}, _optional?: Record<string, never>): Promise<AccessDecision>;
|
|
147
|
+
/** Shadow an unscoped run list with this owner-filtered view: exposing ids would make another
|
|
148
|
+
* principal's runs enumerable. Omit unowned records and refuse a missing principal. */
|
|
125
149
|
export declare function listOwnedRuns<Run extends {
|
|
126
150
|
id: string;
|
|
127
151
|
}>({ runs, registry, principalId, principalHeaderName }: {
|
|
@@ -129,7 +153,7 @@ export declare function listOwnedRuns<Run extends {
|
|
|
129
153
|
registry: RunOwnerRegistry;
|
|
130
154
|
principalId: string | undefined;
|
|
131
155
|
principalHeaderName: string;
|
|
132
|
-
}): {
|
|
156
|
+
}, _optional?: Record<string, never>): {
|
|
133
157
|
allowed: true;
|
|
134
158
|
runs: Run[];
|
|
135
159
|
} | Exclude<AccessDecision, {
|
package/dist/run-credentials.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { timingSafeTokenMatch } from '@jini-ai/core';
|
|
2
2
|
import { createHash, randomBytes, timingSafeEqual } from 'node:crypto';
|
|
3
|
-
/** Explicit Node adapter
|
|
4
|
-
|
|
3
|
+
/** Explicit Node crypto adapter: 32 random bytes produce a 64-hex-character bearer.
|
|
4
|
+
* Length rejection reveals only that fixed public length; content matching is timing-safe.
|
|
5
|
+
* Consumers can inject an alternate implementation. */
|
|
6
|
+
export function createNodeCredentialCrypto(_args, _optional = {}) {
|
|
5
7
|
return {
|
|
6
8
|
mintToken: () => randomBytes(32).toString('hex'),
|
|
7
9
|
digest: ({ token }) => createHash('sha256').update(token, 'utf8').digest('hex'),
|
|
@@ -10,7 +12,7 @@ export function createNodeCredentialCrypto(_args) {
|
|
|
10
12
|
},
|
|
11
13
|
};
|
|
12
14
|
}
|
|
13
|
-
export function createRunScopedCredentials(deps) {
|
|
15
|
+
export function createRunScopedCredentials(deps, _optional = {}) {
|
|
14
16
|
const runByDigest = new Map();
|
|
15
17
|
const tokens = new Map();
|
|
16
18
|
const resolveCaller = ({ token }) => {
|
|
@@ -46,7 +48,9 @@ export function createRunScopedCredentials(deps) {
|
|
|
46
48
|
},
|
|
47
49
|
};
|
|
48
50
|
}
|
|
49
|
-
|
|
51
|
+
/** Preserve an operator token or mint one before spawning a child, which inherits env at spawn.
|
|
52
|
+
* Repeated calls must not rotate the value and invalidate a child that already received it. */
|
|
53
|
+
export function ensureAgentDaemonToken({ env, envVarName, crypto }, _optional = {}) {
|
|
50
54
|
const existing = env[envVarName];
|
|
51
55
|
if (typeof existing === 'string' && existing.length > 0)
|
|
52
56
|
return existing;
|
|
@@ -57,7 +61,7 @@ export function ensureAgentDaemonToken({ env, envVarName, crypto }) {
|
|
|
57
61
|
return token;
|
|
58
62
|
}
|
|
59
63
|
/** Host supplies route names; this module owns matching and run isolation. */
|
|
60
|
-
export function createRouteAccessPolicy({ runPathPrefix, delegatedToolCallsPath, eventStreamSuffix, allowedRoutes }) {
|
|
64
|
+
export function createRouteAccessPolicy({ runPathPrefix, delegatedToolCallsPath, eventStreamSuffix, allowedRoutes }, _optional = {}) {
|
|
61
65
|
return {
|
|
62
66
|
targetRun({ path }) {
|
|
63
67
|
// Binding is case insensitive as in the original gate, while the allowlist remains exact.
|
|
@@ -80,6 +84,12 @@ function notFound(runId, stream) {
|
|
|
80
84
|
return { allowed: false, status: 404, body: { error: { code: 'NOT_FOUND', message: stream ? 'run was not found' : `run "${runId}" was not found` } } };
|
|
81
85
|
}
|
|
82
86
|
/** Transport-independent bearer gate. Apply principalHeader only after an allowed decision. */
|
|
87
|
+
/** Loopback is not authentication: another local process can reach the daemon. No peer-address
|
|
88
|
+
* exemption is allowed. Missing configuration refuses service (503); bad credentials receive 401.
|
|
89
|
+
* Mount authentication before body parsing and ownership checks. Explicit path exemptions use
|
|
90
|
+
* exact equality and apply only without a credential; presenting one always opts into validation.
|
|
91
|
+
* A run bearer is bound to its run before route allowlisting and overwrites caller identity.
|
|
92
|
+
* Mount the delegated-body gate after parsing so body.runId must also match that credential. */
|
|
83
93
|
export function authorizeDaemonRequest({ request, env, envVarName, principalHeaderName, authorizationHeaderName, crypto, routes }, { exemptPaths = [], runScopedCallers, validateDelegatedRunId = false } = {}) {
|
|
84
94
|
const authorization = request.headers[authorizationHeaderName];
|
|
85
95
|
if (exemptPaths.includes(request.path) && authorization === undefined)
|
|
@@ -115,7 +125,7 @@ export function authorizeDaemonRequest({ request, env, envVarName, principalHead
|
|
|
115
125
|
}
|
|
116
126
|
return { allowed: true, principalHeader: { name: principalHeaderName, value: caller.principalId } };
|
|
117
127
|
}
|
|
118
|
-
export function createRunOwnerRegistry(_args) {
|
|
128
|
+
export function createRunOwnerRegistry(_args, _optional = {}) {
|
|
119
129
|
const owners = new Map();
|
|
120
130
|
return {
|
|
121
131
|
record: ({ runId, principalId }) => { owners.set(runId, principalId); },
|
|
@@ -127,7 +137,11 @@ function principalRequired(principalHeaderName) {
|
|
|
127
137
|
return { allowed: false, status: 401, body: { error: { code: 'UNAUTHENTICATED', message: `${principalHeaderName} is required on run-scoped requests` } } };
|
|
128
138
|
}
|
|
129
139
|
/** Mount after the bearer gate; principalId must be server verified. */
|
|
130
|
-
|
|
140
|
+
/** Trust principal headers only downstream of the bearer gate. Missing identity fails closed.
|
|
141
|
+
* Non-owners receive the exact unknown-run response (including the JSON/SSE distinction), so
|
|
142
|
+
* refusal cannot confirm existence. An existing unowned run or failed lookup also denies; only
|
|
143
|
+
* a genuinely absent run passes to the route for its own 404. */
|
|
144
|
+
export async function authorizeRunOwnership({ runId, principalId, principalHeaderName, registry, runExists, eventStream }, _optional = {}) {
|
|
131
145
|
if (!principalId)
|
|
132
146
|
return principalRequired(principalHeaderName);
|
|
133
147
|
if (!runId)
|
|
@@ -144,7 +158,9 @@ export async function authorizeRunOwnership({ runId, principalId, principalHeade
|
|
|
144
158
|
catch { /* Unknown ownership fails closed on lookup failure. */ }
|
|
145
159
|
return exists ? notFound(runId, eventStream) : { allowed: true };
|
|
146
160
|
}
|
|
147
|
-
|
|
161
|
+
/** Shadow an unscoped run list with this owner-filtered view: exposing ids would make another
|
|
162
|
+
* principal's runs enumerable. Omit unowned records and refuse a missing principal. */
|
|
163
|
+
export function listOwnedRuns({ runs, registry, principalId, principalHeaderName }, _optional = {}) {
|
|
148
164
|
if (!principalId)
|
|
149
165
|
return principalRequired(principalHeaderName);
|
|
150
166
|
return { allowed: true, runs: runs.filter(run => registry.ownerOf({ runId: run.id }) === principalId) };
|
package/dist/run-lifecycle.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ export interface StartRunInput {
|
|
|
15
15
|
readonly idempotencyKey?: string;
|
|
16
16
|
/** Override the generated run id — test/fixture hook only. */
|
|
17
17
|
readonly runId?: string;
|
|
18
|
-
/** When set, arms an inactivity watchdog that fails the run
|
|
18
|
+
/** When set, arms an inactivity watchdog that fails the run permanently if no `emit()` occurs within this window. */
|
|
19
19
|
readonly inactivityTimeoutMs?: number;
|
|
20
20
|
}
|
|
21
21
|
export interface StartRunResult {
|
package/dist/run-lifecycle.js
CHANGED
|
@@ -450,10 +450,9 @@ export function createRunLifecycle(requiredArgs, optionalArgs = {}) {
|
|
|
450
450
|
}
|
|
451
451
|
/**
|
|
452
452
|
* Fires when a run's inactivity watchdog times out with no intervening
|
|
453
|
-
* `emit()`/`finish()`.
|
|
454
|
-
*
|
|
455
|
-
*
|
|
456
|
-
* treats timeout/inactivity as one of only two resumable categories).
|
|
453
|
+
* `emit()`/`finish()`. A deliberate watchdog kill is permanent: automatically restarting
|
|
454
|
+
* it would undo the operator's inactivity policy. Other unexpected process failures still
|
|
455
|
+
* retain their original runtime classification.
|
|
457
456
|
*
|
|
458
457
|
* The `!record || isTerminalRunState(...)` guard below is defensive only and is currently
|
|
459
458
|
* unreachable through the public API — deliberately kept, not fake-tested, and the reason is
|
|
@@ -479,7 +478,9 @@ export function createRunLifecycle(requiredArgs, optionalArgs = {}) {
|
|
|
479
478
|
return;
|
|
480
479
|
}
|
|
481
480
|
try {
|
|
482
|
-
|
|
481
|
+
// A deliberate inactivity kill is permanent for this attempt, not an auto-resume signal.
|
|
482
|
+
await lifecycle.emit({ runId, input: { event: 'agent', data: { type: 'status', label: 'Stopped by inactivity watchdog', detail: 'inactivity-watchdog' } } });
|
|
483
|
+
await lifecycle.finish({ runId, status: 'failed', code: null, signal: null, resumable: false });
|
|
483
484
|
}
|
|
484
485
|
catch (error) {
|
|
485
486
|
const context = { source: 'inactivity-timeout', runId, error };
|
package/dist/scheduler.d.ts
CHANGED
|
@@ -5,4 +5,9 @@ export interface SchedulerPort {
|
|
|
5
5
|
readonly callback: () => void;
|
|
6
6
|
}): () => void;
|
|
7
7
|
}
|
|
8
|
+
/** Native host timers; an abandoned exchange must not keep a Node process alive by itself.
|
|
9
|
+
* Cancellation is idempotent, and scheduling never invokes the callback synchronously. */
|
|
10
|
+
export declare function createTimeoutScheduler(_required: Record<string, never>, { keepAlive }?: {
|
|
11
|
+
readonly keepAlive?: boolean;
|
|
12
|
+
}): SchedulerPort;
|
|
8
13
|
//# sourceMappingURL=scheduler.d.ts.map
|
package/dist/scheduler.js
CHANGED
|
@@ -1,2 +1,14 @@
|
|
|
1
|
-
|
|
1
|
+
/** Native host timers; an abandoned exchange must not keep a Node process alive by itself.
|
|
2
|
+
* Cancellation is idempotent, and scheduling never invokes the callback synchronously. */
|
|
3
|
+
export function createTimeoutScheduler(_required, { keepAlive = false } = {}) {
|
|
4
|
+
return {
|
|
5
|
+
schedule({ delayMs, callback }) {
|
|
6
|
+
const timer = setTimeout(callback, delayMs);
|
|
7
|
+
// Exchanges may be abandoned; active start gates must keep their timeout alive until settlement.
|
|
8
|
+
if (!keepAlive)
|
|
9
|
+
timer.unref?.();
|
|
10
|
+
return () => clearTimeout(timer);
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
}
|
|
2
14
|
//# sourceMappingURL=scheduler.js.map
|