@intx/harness 0.2.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/README.md CHANGED
@@ -17,15 +17,19 @@ import {
17
17
  } from "@intx/agent";
18
18
  import { noopAuditStore, permissiveAuthorize } from "@intx/agent/testing";
19
19
  import { createHarness, defineMailTools } from "@intx/harness";
20
- import { createIsogitStore } from "@intx/storage-isogit";
20
+ import { createIsogitStore } from "@intx/storage-isogit/node";
21
21
 
22
- const mailFactory = defineMailTools(() => ({
23
- definitions: myMailTools.definitions,
24
- run: (call, signal) => myMailTools.run(call, signal),
25
- }));
22
+ const mailFactory = defineMailTools(
23
+ () => ({
24
+ definitions: myMailTools.definitions,
25
+ run: (call, signal) => myMailTools.run(call, signal),
26
+ }),
27
+ myMailTools.definitions.map((def) => ({ name: def.name })),
28
+ );
26
29
 
27
30
  const posixFactory = defineTool({
28
31
  id: "@my-org/agent/posix",
32
+ definitions: myPosixTools.definitions.map((def) => ({ name: def.name })),
29
33
  factory: () => ({
30
34
  definitions: myPosixTools.definitions,
31
35
  run: (call, signal) => myPosixTools.run(call, signal),
@@ -0,0 +1,65 @@
1
+ import { type GrantRule } from "@intx/authz";
2
+ import type { CredentialCapability, CredentialMaterialSource } from "@intx/types";
3
+ import type { ToolCredentialDeclaration } from "@intx/types/package-json";
4
+ import type { CredentialProviderRegistry } from "./credential-providers.js";
5
+ /**
6
+ * A binding resolved at launch: which credential backs a declared handle, which
7
+ * provider shapes it, the origin it authenticates to, and how to read its
8
+ * current material. The material source is an indirection over a mutable cell so
9
+ * a rotation reaches an already-shaped handle without a rebuild.
10
+ */
11
+ export interface ResolvedCredentialBinding {
12
+ /** The credential row id the handle resolved to; the `credential:{id}` the
13
+ * use-grant check runs against. */
14
+ credentialId: string;
15
+ /** The provider plugin key that shapes this credential's handle. */
16
+ providerKey: string;
17
+ /** The provider origin the shaped handle authenticates to. */
18
+ origin: string;
19
+ /** Reads the current secret material (rotation indirection). */
20
+ readCurrentMaterial: CredentialMaterialSource;
21
+ }
22
+ /**
23
+ * Reconcile a tool package's declared credential handles (its C5 `interchange.
24
+ * credentials`) against the handles a binding actually resolved for it. A
25
+ * declared handle with no binding is a launch-blocking misconfiguration -- the
26
+ * tool needs a credential the definition never bound -- so this fails the launch
27
+ * loudly rather than letting the gap surface as a resolve-time throw at the
28
+ * tool's first use. It is the throw-on-missing of `resolve`, pulled earlier to
29
+ * launch where the whole set is known.
30
+ */
31
+ export declare function reconcileDeclaredCredentials(consumer: string, declared: readonly ToolCredentialDeclaration[], boundHandles: ReadonlySet<string>): void;
32
+ export interface CredentialCapabilityDeps {
33
+ /**
34
+ * The consumer identity of the tool package this capability serves
35
+ * (`tool:<package>`, from `toolConsumer`). Gate 2 checks each grant's
36
+ * `{ tool }` condition against this value; an empty identity fails closed.
37
+ */
38
+ consumer: string;
39
+ /** Resolved bindings keyed by the handle the tool declared. */
40
+ bindings: ReadonlyMap<string, ResolvedCredentialBinding>;
41
+ /** The registry that shapes a credential into a mediated handle. */
42
+ providers: CredentialProviderRegistry;
43
+ /** The grants in effect for this deploy (the consumer's run grants). */
44
+ grants: GrantRule[];
45
+ }
46
+ /**
47
+ * A `CredentialCapability` plus a host-only `dispose`. The tool sees only
48
+ * `resolve`; the host runs `dispose` on teardown to release every handle shaped
49
+ * through this capability (an http handle holds nothing; a future key-file /
50
+ * socket handle would).
51
+ */
52
+ export interface HostCredentialCapability extends CredentialCapability {
53
+ dispose(): Promise<void>;
54
+ }
55
+ /**
56
+ * Build the consumer-gated `credentials` capability for one tool package.
57
+ *
58
+ * `resolve(handle)` fails closed at every step: an unbound handle throws; a
59
+ * handle the consumer is not authorized to use throws (Gate 2 -- the same
60
+ * `authorizeAction` the model-source path uses, here supplied the credential-use
61
+ * condition registry and this consumer). Only an authorized handle is shaped,
62
+ * once, and memoized so repeated resolves return the same instance and there is
63
+ * a single thing to dispose.
64
+ */
65
+ export declare function createCredentialCapability(deps: CredentialCapabilityDeps): HostCredentialCapability;
@@ -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
+ }
package/dist/harness.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type Agent, type AgentDefinition, type AnnotatedToolFactory, type BaseEnv, type ToolBundle } from "@intx/agent";
1
+ import { type Agent, type AgentDefinition, type AnnotatedToolFactory, type BaseEnv, type ToolBundle, type ToolDeclaration } from "@intx/agent";
2
2
  import type { BlobReader, ConnectorThreadState, ContextStore, InboundMessage, InferenceSource, MessageTransport } from "@intx/types/runtime";
3
3
  import { createConnectorRouter } from "./connector-router.js";
4
4
  /**
@@ -167,8 +167,14 @@ export declare function createWrappedStorageOverrides(baseStorage: ContextStore,
167
167
  * the "what does the harness need vs. what does the tool runner
168
168
  * need" partition onto the caller, which is exactly the partition
169
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.
170
176
  */
171
- export declare function defineMailTools(wrapper: MailToolWrapper): AnnotatedToolFactory<MailEnv>;
177
+ export declare function defineMailTools(wrapper: MailToolWrapper, definitions: readonly ToolDeclaration[]): AnnotatedToolFactory<MailEnv>;
172
178
  /**
173
179
  * Construct a composition-layer agent: the underlying agent wrapped
174
180
  * with connector-state-aware storage, transport subscription, INBOX
package/dist/harness.js CHANGED
@@ -146,11 +146,18 @@ export function createWrappedStorageOverrides(baseStorage, connectorRouter, isIn
146
146
  * the "what does the harness need vs. what does the tool runner
147
147
  * need" partition onto the caller, which is exactly the partition
148
148
  * this helper exists to hide.
149
+ *
150
+ * `definitions` is the static declaration `defineTool` requires: the
151
+ * tool names this factory contributes, enumerable without invoking the
152
+ * wrapper. The caller supplies it because the wrapper binds `transport`
153
+ * from env and cannot run at declaration time; the caller already holds
154
+ * the mail-tool runner whose `definitions` name the same tools.
149
155
  */
150
- export function defineMailTools(wrapper) {
156
+ export function defineMailTools(wrapper, definitions) {
151
157
  return defineTool({
152
158
  id: "@intx/harness/mail",
153
159
  requires: ["transport", "address"],
160
+ definitions,
154
161
  factory: (env) => {
155
162
  const bundle = wrapper(env.transport);
156
163
  return {
package/dist/index.d.ts CHANGED
@@ -1,5 +1,9 @@
1
1
  export { createHarness, defineMailTools, type Harness, type MailEnv, type MailToolWrapper, } from "./harness.js";
2
2
  export { createHarnessRuntimeCapabilities } from "./runtime-capabilities.js";
3
3
  export type { HarnessRuntimeCapabilitiesOptions } from "./runtime-capabilities.js";
4
+ export { createCredentialProviderRegistry, createHttpCredentialProvider, builtinCredentialProviders, } from "./credential-providers.js";
5
+ export type { CredentialProviderRegistry, FetchLike, HttpCredentialProviderOptions, } from "./credential-providers.js";
6
+ export { createCredentialCapability, reconcileDeclaredCredentials, } from "./credential-capability.js";
7
+ export type { CredentialCapabilityDeps, HostCredentialCapability, ResolvedCredentialBinding, } from "./credential-capability.js";
4
8
  export { createConnectorRouter, NoActiveConnectorThreadError, } from "./connector-router.js";
5
9
  export type { ConnectorRouter, ConnectorReplyParts, ConnectorRouterOptions, RouteDecision, } from "./connector-router.js";
package/dist/index.js CHANGED
@@ -1,3 +1,5 @@
1
1
  export { createHarness, defineMailTools, } from "./harness.js";
2
2
  export { createHarnessRuntimeCapabilities } from "./runtime-capabilities.js";
3
+ export { createCredentialProviderRegistry, createHttpCredentialProvider, builtinCredentialProviders, } from "./credential-providers.js";
4
+ export { createCredentialCapability, reconcileDeclaredCredentials, } from "./credential-capability.js";
3
5
  export { createConnectorRouter, NoActiveConnectorThreadError, } from "./connector-router.js";
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@intx/harness",
3
- "version": "0.2.2",
3
+ "description": "Mail-transport composition layer over @intx/agent adding INBOX watch and connector routing",
4
+ "version": "0.3.0",
4
5
  "license": "LGPL-2.1-only",
5
6
  "type": "module",
6
7
  "exports": {
@@ -11,14 +12,15 @@
11
12
  }
12
13
  },
13
14
  "dependencies": {
14
- "@intx/agent": "0.2.2",
15
- "@intx/log": "0.2.2",
16
- "@intx/types": "0.2.2"
15
+ "@intx/agent": "0.3.0",
16
+ "@intx/authz": "0.3.0",
17
+ "@intx/log": "0.3.0",
18
+ "@intx/types": "0.3.0"
17
19
  },
18
20
  "devDependencies": {
19
- "@intx/inference-testing": "0.2.2",
20
- "@intx/mime": "0.2.2",
21
- "@intx/storage-isogit": "0.2.2",
21
+ "@intx/inference-testing": "0.3.0",
22
+ "@intx/mime": "0.3.0",
23
+ "@intx/storage-isogit": "0.3.0",
22
24
  "arktype": "^2.1.29"
23
25
  },
24
26
  "files": [