@intx/harness 0.1.2 → 0.3.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/LICENSE +176 -0
- package/README.md +60 -27
- package/dist/connector-router.d.ts +86 -0
- package/dist/connector-router.js +181 -0
- package/dist/credential-capability.d.ts +65 -0
- package/dist/credential-capability.js +98 -0
- package/dist/credential-providers.d.ts +56 -0
- package/dist/credential-providers.js +123 -0
- package/dist/harness.d.ts +188 -0
- package/dist/harness.js +446 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +5 -0
- package/dist/runtime-capabilities.d.ts +6 -0
- package/dist/runtime-capabilities.js +10 -0
- package/package.json +23 -7
- package/src/config.ts +0 -135
- package/src/connector-router.test.ts +0 -718
- package/src/connector-router.ts +0 -304
- package/src/deploy-tree.test.ts +0 -51
- package/src/deploy-tree.ts +0 -35
- package/src/harness.test.ts +0 -1747
- package/src/harness.ts +0 -379
- package/src/index.ts +0 -31
- package/src/merge-tool-runners.test.ts +0 -149
- package/src/merge-tool-runners.ts +0 -90
- package/src/runtime-capabilities.test.ts +0 -19
- package/src/runtime-capabilities.ts +0 -22
- package/tsconfig.json +0 -4
- package/tsconfig.tsbuildinfo +0 -1
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// The consumer-gated `credentials` capability: the sub-registry a tool queries
|
|
2
|
+
// by its declared handle to obtain a mediated credential. It is the runtime
|
|
3
|
+
// gate that enforces the `{ tool }` condition on a materialized
|
|
4
|
+
// `credential:{id}` / `use` grant -- the check the launch-time grant
|
|
5
|
+
// materialization sets up but does not itself evaluate.
|
|
6
|
+
//
|
|
7
|
+
// The gate lives here, at the point of use, and fails closed: a handle resolves
|
|
8
|
+
// only when the calling consumer holds `credential:{id}` / `use` with the
|
|
9
|
+
// grant's `{ tool }` condition matching this consumer. The shaping of the
|
|
10
|
+
// handle is delegated to the provider registry; the material is read fresh per
|
|
11
|
+
// use (rotation indirection) from the source the binding carries.
|
|
12
|
+
import { authorizeAction, CREDENTIAL_USE_CONDITIONS, } from "@intx/authz";
|
|
13
|
+
/**
|
|
14
|
+
* Reconcile a tool package's declared credential handles (its C5 `interchange.
|
|
15
|
+
* credentials`) against the handles a binding actually resolved for it. A
|
|
16
|
+
* declared handle with no binding is a launch-blocking misconfiguration -- the
|
|
17
|
+
* tool needs a credential the definition never bound -- so this fails the launch
|
|
18
|
+
* loudly rather than letting the gap surface as a resolve-time throw at the
|
|
19
|
+
* tool's first use. It is the throw-on-missing of `resolve`, pulled earlier to
|
|
20
|
+
* launch where the whole set is known.
|
|
21
|
+
*/
|
|
22
|
+
export function reconcileDeclaredCredentials(consumer, declared, boundHandles) {
|
|
23
|
+
const missing = declared
|
|
24
|
+
.map((declaration) => declaration.handle)
|
|
25
|
+
.filter((handle) => !boundHandles.has(handle));
|
|
26
|
+
if (missing.length > 0) {
|
|
27
|
+
throw new Error(`consumer ${consumer} declares credential handle(s) that no binding resolves: ${missing.join(", ")}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Build the consumer-gated `credentials` capability for one tool package.
|
|
32
|
+
*
|
|
33
|
+
* `resolve(handle)` fails closed at every step: an unbound handle throws; a
|
|
34
|
+
* handle the consumer is not authorized to use throws (Gate 2 -- the same
|
|
35
|
+
* `authorizeAction` the model-source path uses, here supplied the credential-use
|
|
36
|
+
* condition registry and this consumer). Only an authorized handle is shaped,
|
|
37
|
+
* once, and memoized so repeated resolves return the same instance and there is
|
|
38
|
+
* a single thing to dispose.
|
|
39
|
+
*/
|
|
40
|
+
export function createCredentialCapability(deps) {
|
|
41
|
+
// Memoize the in-flight PROMISE, not the resolved handle, so two concurrent
|
|
42
|
+
// resolves of the same handle share one gate+shape and yield one instance
|
|
43
|
+
// (caching the value would let both miss the memo and shape twice, orphaning
|
|
44
|
+
// a handle). A deterministic failure -- unbound handle, denied gate, unknown
|
|
45
|
+
// provider -- caches too; it stays failed for this deploy, which is correct
|
|
46
|
+
// since grants do not change mid-deploy.
|
|
47
|
+
const shaped = new Map();
|
|
48
|
+
function shapeHandle(handle) {
|
|
49
|
+
return (async () => {
|
|
50
|
+
const binding = deps.bindings.get(handle);
|
|
51
|
+
if (binding === undefined) {
|
|
52
|
+
throw new Error(`no credential is bound to handle "${handle}" for consumer ${deps.consumer}`);
|
|
53
|
+
}
|
|
54
|
+
// Gate 2: fail closed unless the consumer holds credential:{id} / use with
|
|
55
|
+
// the grant's { tool } condition matching this consumer.
|
|
56
|
+
const decision = await authorizeAction(deps.grants, `credential:${binding.credentialId}`, "use", { registry: CREDENTIAL_USE_CONDITIONS, consumer: deps.consumer });
|
|
57
|
+
if (!decision.ok) {
|
|
58
|
+
throw new Error(`consumer ${deps.consumer} is not authorized to use credential ${binding.credentialId} (${decision.reason})`);
|
|
59
|
+
}
|
|
60
|
+
const provider = deps.providers.resolve(binding.providerKey);
|
|
61
|
+
return provider.shape({
|
|
62
|
+
origin: binding.origin,
|
|
63
|
+
readCurrentMaterial: binding.readCurrentMaterial,
|
|
64
|
+
});
|
|
65
|
+
})();
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
resolve(handle) {
|
|
69
|
+
const existing = shaped.get(handle);
|
|
70
|
+
if (existing !== undefined)
|
|
71
|
+
return existing;
|
|
72
|
+
const pending = shapeHandle(handle);
|
|
73
|
+
shaped.set(handle, pending);
|
|
74
|
+
return pending;
|
|
75
|
+
},
|
|
76
|
+
async dispose() {
|
|
77
|
+
// Dispose EVERY successfully-shaped handle even if one throws -- a single
|
|
78
|
+
// bad handle must not strand the rest -- then surface any failures loudly
|
|
79
|
+
// rather than swallowing them.
|
|
80
|
+
const settled = await Promise.allSettled([...shaped.values()]);
|
|
81
|
+
shaped.clear();
|
|
82
|
+
const errors = [];
|
|
83
|
+
for (const result of settled) {
|
|
84
|
+
if (result.status !== "fulfilled")
|
|
85
|
+
continue;
|
|
86
|
+
try {
|
|
87
|
+
await result.value.dispose();
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
errors.push(error);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (errors.length > 0) {
|
|
94
|
+
throw new AggregateError(errors, "one or more credential handles failed to dispose");
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { CredentialProvider } from "@intx/types";
|
|
2
|
+
/** Resolves a provider identifier to the plugin that shapes its handles. */
|
|
3
|
+
export interface CredentialProviderRegistry {
|
|
4
|
+
has(key: string): boolean;
|
|
5
|
+
resolve(key: string): CredentialProvider;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Build a registry from a list of providers. The list is copied into a private
|
|
9
|
+
* `Map`, so callers cannot mutate the set after construction and lookups never
|
|
10
|
+
* reach `Object.prototype`. A duplicate key is a wiring error and throws at
|
|
11
|
+
* construction rather than silently shadowing.
|
|
12
|
+
*/
|
|
13
|
+
export declare function createCredentialProviderRegistry(providers: readonly CredentialProvider[]): CredentialProviderRegistry;
|
|
14
|
+
/**
|
|
15
|
+
* The minimal call signature the shaped handle needs from `fetch`. The global
|
|
16
|
+
* `fetch` satisfies it; a test stub can too, without implementing the extra
|
|
17
|
+
* members (`preconnect`) the full `fetch` type carries.
|
|
18
|
+
*/
|
|
19
|
+
export type FetchLike = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
20
|
+
/** Options for the built-in HTTP provider. */
|
|
21
|
+
export interface HttpCredentialProviderOptions {
|
|
22
|
+
/**
|
|
23
|
+
* The `fetch` the shaped handle delegates to once the request is
|
|
24
|
+
* origin-checked and the auth header is injected. Defaults to the global
|
|
25
|
+
* `fetch`; injectable so origin-pinning can be exercised without a network.
|
|
26
|
+
*/
|
|
27
|
+
fetch?: FetchLike;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The built-in HTTP credential provider. It shapes an `HttpMediatedCredential`:
|
|
31
|
+
* an authed `fetch` pinned to the credential's provider origin, injecting the
|
|
32
|
+
* current secret as a bearer token per request. The material is read fresh on
|
|
33
|
+
* every call, so a rotation that updates the underlying cell is picked up
|
|
34
|
+
* without rebuilding the handle.
|
|
35
|
+
*
|
|
36
|
+
* Origin pinning is load-bearing security: the handle authenticates only the
|
|
37
|
+
* initial, origin-checked request and never follows redirects. A request whose
|
|
38
|
+
* resolved origin is not the pinned one is refused, and a server 3xx is
|
|
39
|
+
* returned to the caller unfollowed (`redirect: "manual"`), so the bearer is
|
|
40
|
+
* never sent to any origin but the pinned one. Transparent redirect-following
|
|
41
|
+
* is intentionally not provided: a tool re-issues a same-origin redirect target
|
|
42
|
+
* through the handle (a cross-origin one is refused). This keeps token safety
|
|
43
|
+
* in the handle rather than resting on the injected `fetch`'s redirect
|
|
44
|
+
* behavior.
|
|
45
|
+
*
|
|
46
|
+
* Bearer is the only auth scheme today; providers that authenticate differently
|
|
47
|
+
* (a `token` scheme, an `x-api-key` header) are separate plugins, not a branch
|
|
48
|
+
* here.
|
|
49
|
+
*/
|
|
50
|
+
export declare function createHttpCredentialProvider(opts?: HttpCredentialProviderOptions): CredentialProvider;
|
|
51
|
+
/**
|
|
52
|
+
* The built-in credential providers every host registers. A single `http`
|
|
53
|
+
* provider today; a host composes additional providers by extending the list
|
|
54
|
+
* passed to `createCredentialProviderRegistry`.
|
|
55
|
+
*/
|
|
56
|
+
export declare function builtinCredentialProviders(): CredentialProvider[];
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// Credential provider plugins: the seam that shapes a resolved provider-backed
|
|
2
|
+
// credential into a mediated handle a consumer can use. A provider owns HOW the
|
|
3
|
+
// handle authenticates (an authed `fetch`, a future key-file + socket); it is
|
|
4
|
+
// given a material source and never acquires material or decides authorization
|
|
5
|
+
// -- both happen upstream, at the delivery boundary, before a provider is
|
|
6
|
+
// consulted.
|
|
7
|
+
//
|
|
8
|
+
// The registry mirrors @intx/inference's AdapterRegistry: a Map-backed lookup
|
|
9
|
+
// keyed by provider identifier, prototype-pollution-safe (a Map never consults
|
|
10
|
+
// Object.prototype, so an untrusted key like "toString" resolves to the loud
|
|
11
|
+
// unknown-provider error rather than an inherited member), throw-on-missing.
|
|
12
|
+
/**
|
|
13
|
+
* Build a registry from a list of providers. The list is copied into a private
|
|
14
|
+
* `Map`, so callers cannot mutate the set after construction and lookups never
|
|
15
|
+
* reach `Object.prototype`. A duplicate key is a wiring error and throws at
|
|
16
|
+
* construction rather than silently shadowing.
|
|
17
|
+
*/
|
|
18
|
+
export function createCredentialProviderRegistry(providers) {
|
|
19
|
+
const byKey = new Map();
|
|
20
|
+
for (const provider of providers) {
|
|
21
|
+
if (byKey.has(provider.key)) {
|
|
22
|
+
throw new Error(`Duplicate credential provider key: ${provider.key}`);
|
|
23
|
+
}
|
|
24
|
+
byKey.set(provider.key, provider);
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
has(key) {
|
|
28
|
+
return byKey.has(key);
|
|
29
|
+
},
|
|
30
|
+
resolve(key) {
|
|
31
|
+
const provider = byKey.get(key);
|
|
32
|
+
if (provider === undefined) {
|
|
33
|
+
throw new Error(`Unknown credential provider: ${key}`);
|
|
34
|
+
}
|
|
35
|
+
return provider;
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The built-in HTTP credential provider. It shapes an `HttpMediatedCredential`:
|
|
41
|
+
* an authed `fetch` pinned to the credential's provider origin, injecting the
|
|
42
|
+
* current secret as a bearer token per request. The material is read fresh on
|
|
43
|
+
* every call, so a rotation that updates the underlying cell is picked up
|
|
44
|
+
* without rebuilding the handle.
|
|
45
|
+
*
|
|
46
|
+
* Origin pinning is load-bearing security: the handle authenticates only the
|
|
47
|
+
* initial, origin-checked request and never follows redirects. A request whose
|
|
48
|
+
* resolved origin is not the pinned one is refused, and a server 3xx is
|
|
49
|
+
* returned to the caller unfollowed (`redirect: "manual"`), so the bearer is
|
|
50
|
+
* never sent to any origin but the pinned one. Transparent redirect-following
|
|
51
|
+
* is intentionally not provided: a tool re-issues a same-origin redirect target
|
|
52
|
+
* through the handle (a cross-origin one is refused). This keeps token safety
|
|
53
|
+
* in the handle rather than resting on the injected `fetch`'s redirect
|
|
54
|
+
* behavior.
|
|
55
|
+
*
|
|
56
|
+
* Bearer is the only auth scheme today; providers that authenticate differently
|
|
57
|
+
* (a `token` scheme, an `x-api-key` header) are separate plugins, not a branch
|
|
58
|
+
* here.
|
|
59
|
+
*/
|
|
60
|
+
export function createHttpCredentialProvider(opts) {
|
|
61
|
+
const fetchImpl = opts?.fetch ?? globalThis.fetch;
|
|
62
|
+
return {
|
|
63
|
+
key: "http",
|
|
64
|
+
shape(context) {
|
|
65
|
+
const pinnedOrigin = new URL(context.origin).origin;
|
|
66
|
+
return {
|
|
67
|
+
kind: "http",
|
|
68
|
+
async fetch(input, init) {
|
|
69
|
+
const target = resolveTargetUrl(input, pinnedOrigin);
|
|
70
|
+
if (target.origin !== pinnedOrigin) {
|
|
71
|
+
throw new Error(`http credential is pinned to ${pinnedOrigin}; refusing cross-origin request to ${target.origin}`);
|
|
72
|
+
}
|
|
73
|
+
// Read the secret fresh on every call so a rotation of the underlying
|
|
74
|
+
// material cell reaches this handle without a rebuild.
|
|
75
|
+
const { secret } = context.readCurrentMaterial();
|
|
76
|
+
// redirect:"manual" is dictated by the handle, never inherited from
|
|
77
|
+
// caller input. The origin check guards only the INITIAL url, so
|
|
78
|
+
// following a server 3xx to a foreign origin would carry the bearer
|
|
79
|
+
// off the pinned host. Instead the 3xx is returned to the caller
|
|
80
|
+
// unfollowed: a same-origin target is re-issued through the handle
|
|
81
|
+
// (which re-pins and re-auths); a cross-origin one is refused above.
|
|
82
|
+
if (input instanceof Request) {
|
|
83
|
+
// Re-issue the caller's request (method, body preserved) with the
|
|
84
|
+
// auth header added and the redirect mode forced; its url was
|
|
85
|
+
// origin-checked above.
|
|
86
|
+
const headers = new Headers(input.headers);
|
|
87
|
+
headers.set("authorization", `Bearer ${secret}`);
|
|
88
|
+
return fetchImpl(new Request(input, { headers, redirect: "manual" }));
|
|
89
|
+
}
|
|
90
|
+
const headers = new Headers(init?.headers);
|
|
91
|
+
headers.set("authorization", `Bearer ${secret}`);
|
|
92
|
+
return fetchImpl(target, { ...init, headers, redirect: "manual" });
|
|
93
|
+
},
|
|
94
|
+
dispose() {
|
|
95
|
+
// An http handle allocates no resources; nothing to release.
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The built-in credential providers every host registers. A single `http`
|
|
103
|
+
* provider today; a host composes additional providers by extending the list
|
|
104
|
+
* passed to `createCredentialProviderRegistry`.
|
|
105
|
+
*/
|
|
106
|
+
export function builtinCredentialProviders() {
|
|
107
|
+
return [createHttpCredentialProvider()];
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Resolve the URL a request targets. A relative string resolves against the
|
|
111
|
+
* pinned origin (so a tool can call `/repos`); an absolute string or URL keeps
|
|
112
|
+
* its own origin (and is refused by the caller if it differs); a `Request`
|
|
113
|
+
* carries an absolute URL already.
|
|
114
|
+
*/
|
|
115
|
+
function resolveTargetUrl(input, pinnedOrigin) {
|
|
116
|
+
if (typeof input === "string") {
|
|
117
|
+
return new URL(input, pinnedOrigin);
|
|
118
|
+
}
|
|
119
|
+
if (input instanceof URL) {
|
|
120
|
+
return input;
|
|
121
|
+
}
|
|
122
|
+
return new URL(input.url);
|
|
123
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { type Agent, type AgentDefinition, type AnnotatedToolFactory, type BaseEnv, type ToolBundle, type ToolDeclaration } from "@intx/agent";
|
|
2
|
+
import type { BlobReader, ConnectorThreadState, ContextStore, InboundMessage, InferenceSource, MessageTransport } from "@intx/types/runtime";
|
|
3
|
+
import { createConnectorRouter } from "./connector-router.js";
|
|
4
|
+
/**
|
|
5
|
+
* Env extension the composition layer requires beyond `BaseEnv`. Tools
|
|
6
|
+
* shipped by this package declare the matching `requires` so
|
|
7
|
+
* `validateEnv` can blame either at the env entry point.
|
|
8
|
+
*
|
|
9
|
+
* `onReplySendFailed` is invoked when the reply drain catches a failure
|
|
10
|
+
* from `connectorRouter.composeReply` or `transport.send` for an
|
|
11
|
+
* outbound `connector.reply`. The reply is dropped and the router
|
|
12
|
+
* state is not advanced; the callback is the only programmatic surface
|
|
13
|
+
* a caller has to observe the loss. Production deployments that need
|
|
14
|
+
* retry semantics layer them on top of this callback.
|
|
15
|
+
*
|
|
16
|
+
* The callback may be synchronous or async; the reply drain awaits its
|
|
17
|
+
* resolution so an async callback's rejection is observed (and logged)
|
|
18
|
+
* rather than surfacing as an unhandled promise rejection.
|
|
19
|
+
*
|
|
20
|
+
* `onReplyDrainTerminated` is invoked when the reply drain's `for await`
|
|
21
|
+
* loop exits abnormally -- the only documented case is a
|
|
22
|
+
* `StreamBackpressureError` thrown by the agent's event stream when the
|
|
23
|
+
* drain's per-consumer buffer overruns `streamBufferMax`. After this
|
|
24
|
+
* fires the harness is no longer forwarding `connector.reply` events to
|
|
25
|
+
* the transport: in-process `agent.send()` callers still resolve, but
|
|
26
|
+
* outbound replies are silently dropped until `close()`. Production
|
|
27
|
+
* deployments that need to alert on this failure mode subscribe via
|
|
28
|
+
* this callback; the harness only emits a `logger.warn` otherwise. The
|
|
29
|
+
* callback may be synchronous or async and is awaited the same way
|
|
30
|
+
* `onReplySendFailed` is, so an async rejection is observed (and
|
|
31
|
+
* logged) rather than escaping as an unhandled rejection.
|
|
32
|
+
*/
|
|
33
|
+
export interface MailEnv extends BaseEnv {
|
|
34
|
+
transport: MessageTransport;
|
|
35
|
+
address: string;
|
|
36
|
+
onConnectorStateChanged?: (state: ConnectorThreadState | null) => void;
|
|
37
|
+
onReplySendFailed?: (cause: unknown) => void | Promise<void>;
|
|
38
|
+
onReplyDrainTerminated?: (cause: unknown) => void | Promise<void>;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Narrowed public surface returned by `createHarness`. `close` is the
|
|
42
|
+
* only direct surface; everything else is a pass-through to the
|
|
43
|
+
* underlying agent. `stream` is exposed so observability consumers can
|
|
44
|
+
* subscribe to the reactor's event stream without having to grab the
|
|
45
|
+
* agent reference.
|
|
46
|
+
*/
|
|
47
|
+
export interface Harness {
|
|
48
|
+
close(): Promise<void>;
|
|
49
|
+
deliver(message: InboundMessage): void;
|
|
50
|
+
setSource(source: InferenceSource): void;
|
|
51
|
+
setSources(sources: InferenceSource[], defaultSource: string): void;
|
|
52
|
+
stream: Agent["stream"];
|
|
53
|
+
readonly blobReader: BlobReader;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Mail-tool factory shape. The `createMailTools` constructor in
|
|
57
|
+
* `@intx/tools-mail` builds a runner from a transport-bearing
|
|
58
|
+
* capability set; the harness wraps that into a single `defineTool`
|
|
59
|
+
* bundle whose `requires` names the env keys the wrapper touches.
|
|
60
|
+
*
|
|
61
|
+
* Callers (e.g. the sidecar) supply the wrapper as a tool factory on
|
|
62
|
+
* their `AgentDefinition`. `createHarness` does not synthesize it
|
|
63
|
+
* internally -- the caller is the layer that knows which mail-tool
|
|
64
|
+
* implementation to use.
|
|
65
|
+
*/
|
|
66
|
+
export type MailToolWrapper = (transport: MessageTransport) => Omit<ToolBundle, "dispose">;
|
|
67
|
+
/**
|
|
68
|
+
* Invoke the caller-supplied `onReplySendFailed` callback and absorb any
|
|
69
|
+
* failure it raises. Extracted from the reply drain so the await-the-
|
|
70
|
+
* callback contract is testable in isolation: a bare invocation would
|
|
71
|
+
* compile (TypeScript admits `async () => void` as satisfying a `void`-
|
|
72
|
+
* returning signature) but would let an async callback's rejection
|
|
73
|
+
* escape as an unhandled promise rejection. Awaiting protects against
|
|
74
|
+
* that; the helper exists so the protection is asserted by a test
|
|
75
|
+
* rather than implied by inspection of the drain.
|
|
76
|
+
*
|
|
77
|
+
* Exported only for the regression test in this package; no external
|
|
78
|
+
* consumer should call it.
|
|
79
|
+
*
|
|
80
|
+
* The export-and-mark-internal shape is the codebase's convention
|
|
81
|
+
* for helpers that exist to make a production-code contract
|
|
82
|
+
* testable in isolation. A separate `@intx/harness/testing`
|
|
83
|
+
* entry-point was considered and rejected: the helper is one
|
|
84
|
+
* try/catch wrapper around the production callback, tightly
|
|
85
|
+
* coupled to the `MailEnv` callback type defined adjacent to it.
|
|
86
|
+
* Moving it would either duplicate the production code in a test
|
|
87
|
+
* module (defeating the point) or require a parallel entry-point
|
|
88
|
+
* whose only export is a single function -- bundler ceremony for a
|
|
89
|
+
* boundary TypeScript cannot enforce anyway, since deep imports
|
|
90
|
+
* (`@intx/harness/src/harness`) reach the same module regardless
|
|
91
|
+
* of what `index.ts` re-exports. The docstring convention is
|
|
92
|
+
* load-bearing here: the marker is the contract.
|
|
93
|
+
*
|
|
94
|
+
* `invokeReplyDrainTerminated` (below) follows the same shape for
|
|
95
|
+
* the same reason.
|
|
96
|
+
*/
|
|
97
|
+
export declare function invokeReplySendFailed(callback: NonNullable<MailEnv["onReplySendFailed"]>, cause: unknown): Promise<void>;
|
|
98
|
+
/**
|
|
99
|
+
* Invoke the caller-supplied `onReplyDrainTerminated` callback and
|
|
100
|
+
* absorb any failure it raises. Mirrors `invokeReplySendFailed`:
|
|
101
|
+
* extracted from the reply drain so the await-the-callback contract
|
|
102
|
+
* is testable in isolation, exported only for the regression test in
|
|
103
|
+
* this package.
|
|
104
|
+
*/
|
|
105
|
+
export declare function invokeReplyDrainTerminated(callback: NonNullable<MailEnv["onReplyDrainTerminated"]>, cause: unknown): Promise<void>;
|
|
106
|
+
/**
|
|
107
|
+
* Build the `load` / `writeMetadata` overrides the harness layers onto
|
|
108
|
+
* `env.storage`. Extracted from `createHarness` so the dirty-bit gating
|
|
109
|
+
* on `load()` is directly testable -- the production path constructs
|
|
110
|
+
* the overrides inline with the same arguments.
|
|
111
|
+
*
|
|
112
|
+
* The `isInMemoryStateAuthoritative` callback is read on every `load`
|
|
113
|
+
* invocation. The harness sets the bit from the router's
|
|
114
|
+
* `onStateChanged` callback so the gate flips on the same tick a
|
|
115
|
+
* commit produces its first state change; subsequent loads (whether
|
|
116
|
+
* driven by reactor recovery, mid-cycle, or anywhere else) leave the
|
|
117
|
+
* router's in-memory snapshot intact rather than blanking it with the
|
|
118
|
+
* pre-commit disk value.
|
|
119
|
+
*
|
|
120
|
+
* Exported for the regression test in this package; no external
|
|
121
|
+
* consumer should call it. Same shape and rationale as
|
|
122
|
+
* `invokeReplySendFailed` and `invokeReplyDrainTerminated` above --
|
|
123
|
+
* the helper is tightly coupled to the dirty-bit gating semantics
|
|
124
|
+
* that live in this module, and a separate testing entry-point
|
|
125
|
+
* would buy bundler ceremony for a boundary TypeScript cannot
|
|
126
|
+
* enforce. The docstring "internal" marker is the contract.
|
|
127
|
+
*/
|
|
128
|
+
export declare function createWrappedStorageOverrides(baseStorage: ContextStore, connectorRouter: ReturnType<typeof createConnectorRouter>, isInMemoryStateAuthoritative: () => boolean): Pick<ContextStore, "load" | "writeMetadata">;
|
|
129
|
+
/**
|
|
130
|
+
* Construct an `AnnotatedToolFactory` for a mail-tool bundle. The
|
|
131
|
+
* factory binds `transport` from env at construction time and produces
|
|
132
|
+
* a bundle whose lifetime is tied to the agent. Disposal of the
|
|
133
|
+
* underlying mail tools is the caller's responsibility (the env is the
|
|
134
|
+
* agent's dependency contract; the caller owns what it puts in env);
|
|
135
|
+
* the agent itself does not call bundle disposers (see the
|
|
136
|
+
* `ToolBundle` contract in `@intx/agent`). Callers that need to
|
|
137
|
+
* dispose mail tools on shutdown retain a reference to the underlying
|
|
138
|
+
* `MailToolWrapper`'s output and invoke its `dispose` directly --
|
|
139
|
+
* routing disposal through the bundle the agent receives would still
|
|
140
|
+
* not fire since the agent never holds it.
|
|
141
|
+
*
|
|
142
|
+
* The `requires: ["transport", "address"]` declaration captures the
|
|
143
|
+
* env-key surface of the entire mail composition path -- the factory
|
|
144
|
+
* body reads `transport`, and `createHarness` (which the caller pairs
|
|
145
|
+
* this factory with) reads `env.address` to label rejected-message
|
|
146
|
+
* log records identifying which agent's router refused the message.
|
|
147
|
+
* No routing decision keys off `env.address` -- the connector router
|
|
148
|
+
* routes on per-message thread state, not on the agent's own
|
|
149
|
+
* address -- so the field is observability-only. It still belongs in
|
|
150
|
+
* `requires` because the harness's log record assumes the field is
|
|
151
|
+
* populated; declaring it here lets the agent's `validateEnv` blame a
|
|
152
|
+
* missing `address` at construction time rather than letting the
|
|
153
|
+
* watch loop discover it under operational load. Callers that hand-
|
|
154
|
+
* build a `defineTool` factory for a different mail-tool runner must
|
|
155
|
+
* remember to surface `address` on their own `requires` if their
|
|
156
|
+
* `createHarness` consumes it -- the agent has no way to deduce
|
|
157
|
+
* composition-layer env requirements from a factory body that does
|
|
158
|
+
* not itself read the field.
|
|
159
|
+
*
|
|
160
|
+
* The `requires` set is fixed at the two keys above by design; this
|
|
161
|
+
* helper is not the extension point for mail-tool runners that need
|
|
162
|
+
* additional env keys. A mail tool that wants to read (say) a tenant
|
|
163
|
+
* identifier from env should drop down to `defineTool` directly,
|
|
164
|
+
* declare its own `requires` with the full surface, and call the
|
|
165
|
+
* underlying mail-tool constructor inside that factory. Folding an
|
|
166
|
+
* additional `requires` parameter into `defineMailTools` would push
|
|
167
|
+
* the "what does the harness need vs. what does the tool runner
|
|
168
|
+
* need" partition onto the caller, which is exactly the partition
|
|
169
|
+
* this helper exists to hide.
|
|
170
|
+
*
|
|
171
|
+
* `definitions` is the static declaration `defineTool` requires: the
|
|
172
|
+
* tool names this factory contributes, enumerable without invoking the
|
|
173
|
+
* wrapper. The caller supplies it because the wrapper binds `transport`
|
|
174
|
+
* from env and cannot run at declaration time; the caller already holds
|
|
175
|
+
* the mail-tool runner whose `definitions` name the same tools.
|
|
176
|
+
*/
|
|
177
|
+
export declare function defineMailTools(wrapper: MailToolWrapper, definitions: readonly ToolDeclaration[]): AnnotatedToolFactory<MailEnv>;
|
|
178
|
+
/**
|
|
179
|
+
* Construct a composition-layer agent: the underlying agent wrapped
|
|
180
|
+
* with connector-state-aware storage, transport subscription, INBOX
|
|
181
|
+
* watch, and connector-reply forwarding.
|
|
182
|
+
*
|
|
183
|
+
* The reactor is wrapped exactly once -- inside `createAgent`.
|
|
184
|
+
* `createHarness` augments env.storage with connector-state load/save
|
|
185
|
+
* and subscribes to the agent's event stream to intercept
|
|
186
|
+
* `connector.reply` events for outbound transport sends.
|
|
187
|
+
*/
|
|
188
|
+
export declare function createHarness<EnvReq extends MailEnv>(def: AgentDefinition<EnvReq>, env: EnvReq): Promise<Harness>;
|