@jini-ai/daemon 0.5.3 → 0.6.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.
Files changed (77) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/dist/agent-executor/contracts.d.ts +45 -14
  3. package/dist/agent-executor/contracts.js +0 -9
  4. package/dist/agent-executor/event-translation.d.ts +2 -2
  5. package/dist/agent-executor/event-translation.js +0 -2
  6. package/dist/agent-executor/events.d.ts +26 -6
  7. package/dist/agent-executor/events.js +198 -11
  8. package/dist/agent-executor/handoff.d.ts +1 -1
  9. package/dist/agent-executor/handoff.js +0 -1
  10. package/dist/agent-executor/index.d.ts +1 -1
  11. package/dist/agent-executor/launch.d.ts +1 -1
  12. package/dist/agent-executor/launch.js +47 -3
  13. package/dist/agent-executor/mcp-bridge.d.ts +2 -2
  14. package/dist/agent-executor/mcp-bridge.js +19 -8
  15. package/dist/agent-executor/model-receipts.d.ts +6 -0
  16. package/dist/agent-executor/model-receipts.js +26 -0
  17. package/dist/agent-executor.d.ts +7 -1665
  18. package/dist/agent-executor.js +7 -3007
  19. package/dist/core/read-only-tools.d.ts +130 -0
  20. package/dist/core/read-only-tools.js +115 -0
  21. package/dist/core/tool-audit.d.ts +131 -0
  22. package/dist/core/tool-audit.js +113 -0
  23. package/dist/http/active-context.d.ts +2 -2
  24. package/dist/http/agents.d.ts +8 -2
  25. package/dist/http/agents.js +6 -2
  26. package/dist/http/component-catalog.d.ts +2 -2
  27. package/dist/http/connectors.d.ts +17 -17
  28. package/dist/http/daemon-status.d.ts +2 -2
  29. package/dist/http/db-ops.d.ts +3 -3
  30. package/dist/http/delegated-tools.d.ts +1 -1
  31. package/dist/http/delegated-tools.js +1 -1
  32. package/dist/http/frontend-sessions.d.ts +1 -1
  33. package/dist/http/health.d.ts +6 -6
  34. package/dist/http/host-tools.d.ts +2 -2
  35. package/dist/http/index.d.ts +2 -2
  36. package/dist/http/index.js +1 -1
  37. package/dist/http/media.d.ts +4 -4
  38. package/dist/http/memory.d.ts +15 -15
  39. package/dist/http/remote-run-events.d.ts +2 -2
  40. package/dist/http/research.d.ts +1 -1
  41. package/dist/http/route-manifest.js +2 -2
  42. package/dist/http/routines.d.ts +7 -7
  43. package/dist/http/run-credentials.d.ts +2 -2
  44. package/dist/http/run-credentials.js +2 -2
  45. package/dist/http/runs.d.ts +31 -6
  46. package/dist/http/runs.js +36 -1
  47. package/dist/http/terminals.d.ts +6 -6
  48. package/dist/http/tool-catalog.d.ts +2 -2
  49. package/dist/http/xai.d.ts +6 -6
  50. package/dist/index.d.ts +75 -571
  51. package/dist/index.js +75 -571
  52. package/dist/read-only-tools.d.ts +1 -129
  53. package/dist/read-only-tools.js +2 -114
  54. package/dist/run-credentials.d.ts +47 -23
  55. package/dist/run-credentials.js +24 -8
  56. package/dist/scheduler.d.ts +5 -0
  57. package/dist/scheduler.js +13 -1
  58. package/dist/session-coordination.d.ts +58 -24
  59. package/dist/session-coordination.js +40 -12
  60. package/dist/store/event-log/sqlite.js +3 -3
  61. package/dist/surface-exchanges.d.ts +207 -61
  62. package/dist/surface-exchanges.js +167 -20
  63. package/dist/tool-audit.d.ts +1 -113
  64. package/dist/tool-audit.js +2 -94
  65. package/dist/tool-recovery/diagnostics.d.ts +132 -0
  66. package/dist/tool-recovery/diagnostics.js +110 -0
  67. package/dist/tool-recovery/federated-refusal.d.ts +92 -0
  68. package/dist/tool-recovery/federated-refusal.js +152 -0
  69. package/dist/tool-recovery/index.d.ts +6 -0
  70. package/dist/tool-recovery/index.js +6 -0
  71. package/dist/tool-recovery/recovery.d.ts +49 -0
  72. package/dist/tool-recovery/recovery.js +419 -0
  73. package/dist/tool-recovery/redaction.d.ts +166 -0
  74. package/dist/tool-recovery/redaction.js +192 -0
  75. package/dist/tool-recovery/stack.d.ts +69 -0
  76. package/dist/tool-recovery/stack.js +62 -0
  77. package/package.json +35 -12
@@ -1,130 +1,2 @@
1
- import { type Principal, type ToolRegistry } from '@jini-ai/core';
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
@@ -1,115 +1,3 @@
1
- import { isReadOnlyTool } from '@jini-ai/core';
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; consumers can inject an alternate crypto implementation. */
16
- export declare function createNodeCredentialCrypto(_args: Record<string, never>): CredentialCryptoPort;
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, {
@@ -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; consumers can inject an alternate crypto implementation. */
4
- export function createNodeCredentialCrypto(_args) {
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
- export function ensureAgentDaemonToken({ env, envVarName, crypto }) {
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
- export async function authorizeRunOwnership({ runId, principalId, principalHeaderName, registry, runExists, eventStream }) {
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
- export function listOwnedRuns({ runs, registry, principalId, principalHeaderName }) {
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) };
@@ -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
- export {};
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