@getfamiliar/shared 0.1.12 → 0.1.15
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/build/AgentRun.d.ts +16 -11
- package/build/AgentRunBus.js +3 -3
- package/build/Config.d.ts +5 -0
- package/build/Config.js +26 -1
- package/build/DockerTools.d.ts +119 -0
- package/build/DockerTools.js +175 -0
- package/build/Event.d.ts +19 -19
- package/build/EventBus.d.ts +2 -2
- package/build/EventBus.js +6 -8
- package/build/HandlerCatalog.d.ts +33 -28
- package/build/HandlerCatalog.js +70 -64
- package/build/HandlerResolution.d.ts +84 -0
- package/build/HandlerResolution.js +105 -0
- package/build/Plugin.d.ts +70 -12
- package/build/PromptPlaceholders.js +3 -8
- package/build/ScheduledSubagent.d.ts +2 -3
- package/build/ScheduledSubagentBus.d.ts +1 -1
- package/build/ScheduledSubagentBus.js +3 -6
- package/build/Schema.d.ts +11 -9
- package/build/Schema.js +28 -14
- package/build/WebServerDefaults.d.ts +9 -0
- package/build/WebServerDefaults.js +9 -0
- package/build/index.d.ts +6 -3
- package/build/index.js +4 -0
- package/build/testing/MockAgentRunBus.js +1 -1
- package/build/testing/MockEventBus.js +1 -1
- package/package.json +1 -1
package/build/AgentRun.d.ts
CHANGED
|
@@ -40,18 +40,19 @@ export interface AgentRunRow {
|
|
|
40
40
|
*/
|
|
41
41
|
readonly parentAgentrunId: string | null;
|
|
42
42
|
/**
|
|
43
|
-
*
|
|
44
|
-
*
|
|
43
|
+
* The handler's address. Root agentruns copy the event's topic;
|
|
44
|
+
* spawned children carry the topic the spawning tool named. For
|
|
45
|
+
* topic `chat:whatsapp` the handler file is
|
|
46
|
+
* `workspace/chat/whatsapp.md` (or `workspace/chat/whatsapp/index.md`).
|
|
45
47
|
*/
|
|
46
48
|
readonly topic: string;
|
|
47
49
|
/**
|
|
48
|
-
*
|
|
49
|
-
* `
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* 2. fallback `workspace/chat/analyze.md`
|
|
50
|
+
* `true` when the topic must resolve to its own handler file;
|
|
51
|
+
* `false` when resolution may fall back up the topic chain
|
|
52
|
+
* (`chat:whatsapp` → `chat`). Root agentruns copy the event's
|
|
53
|
+
* {@link EventRow.isExactTopic}; spawned children are always exact.
|
|
53
54
|
*/
|
|
54
|
-
readonly
|
|
55
|
+
readonly isExactTopic: boolean;
|
|
55
56
|
/**
|
|
56
57
|
* Resolved model id actually used for this run, of the form
|
|
57
58
|
* `<provider>/<modelId>` (e.g. `featherless/zai-org/GLM-5.1`).
|
|
@@ -147,10 +148,14 @@ export interface NewAgentRun {
|
|
|
147
148
|
readonly eventId: string;
|
|
148
149
|
/** FK to the parent agentrun, or omitted/null for the root agentrun. */
|
|
149
150
|
readonly parentAgentrunId?: string | null;
|
|
150
|
-
/**
|
|
151
|
+
/** Handler address — the event's topic for roots, the named topic for children. Required. */
|
|
151
152
|
readonly topic: string;
|
|
152
|
-
/**
|
|
153
|
-
|
|
153
|
+
/**
|
|
154
|
+
* Whether handler resolution must match the topic exactly. Default
|
|
155
|
+
* `true` (the column default) — only the input-event watcher passes
|
|
156
|
+
* `false`, for events whose topic may fall back up the chain.
|
|
157
|
+
*/
|
|
158
|
+
readonly isExactTopic?: boolean;
|
|
154
159
|
/** Defaults to 50; usually inherited from the event. */
|
|
155
160
|
readonly priority?: number;
|
|
156
161
|
/** Initial state; default `"pending"`. */
|
package/build/AgentRunBus.js
CHANGED
|
@@ -44,13 +44,13 @@ export class AgentRunBus {
|
|
|
44
44
|
*/
|
|
45
45
|
async add(run) {
|
|
46
46
|
const result = await this.connection.getPool().query(`INSERT INTO agentruns
|
|
47
|
-
(event_id, parent_agentrun_id, topic,
|
|
47
|
+
(event_id, parent_agentrun_id, topic, is_exact_topic, priority, state, prompt, payload, privileged, calltype)
|
|
48
48
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb, $9, $10)
|
|
49
49
|
RETURNING *`, [
|
|
50
50
|
run.eventId,
|
|
51
51
|
run.parentAgentrunId ?? null,
|
|
52
52
|
run.topic,
|
|
53
|
-
run.
|
|
53
|
+
run.isExactTopic ?? true,
|
|
54
54
|
run.priority ?? 50,
|
|
55
55
|
run.state ?? "pending",
|
|
56
56
|
run.prompt ?? null,
|
|
@@ -339,7 +339,7 @@ function mapRow(raw) {
|
|
|
339
339
|
eventId: raw.event_id,
|
|
340
340
|
parentAgentrunId: raw.parent_agentrun_id,
|
|
341
341
|
topic: raw.topic,
|
|
342
|
-
|
|
342
|
+
isExactTopic: raw.is_exact_topic,
|
|
343
343
|
priority: raw.priority,
|
|
344
344
|
state: raw.state,
|
|
345
345
|
prompt: raw.prompt,
|
package/build/Config.d.ts
CHANGED
|
@@ -19,6 +19,11 @@
|
|
|
19
19
|
* boot. Plugin-side keys are not policed by the platform — plugins own
|
|
20
20
|
* the validation of their own subtrees.
|
|
21
21
|
*/
|
|
22
|
+
/**
|
|
23
|
+
* Fallback for `core.writablePaths` when `config.yml` doesn't set it:
|
|
24
|
+
* the workspace globs the agent may write to, which are never handlers.
|
|
25
|
+
*/
|
|
26
|
+
export declare const DEFAULT_WRITABLE_PATHS: readonly string[];
|
|
22
27
|
export interface ConfigService {
|
|
23
28
|
/**
|
|
24
29
|
* Read a string under a dotted path. Throws when the key is
|
package/build/Config.js
CHANGED
|
@@ -1 +1,26 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Plugin-agnostic configuration surface exposed by the host.
|
|
3
|
+
*
|
|
4
|
+
* Plugins reach configuration through `HostContext.config` rather than
|
|
5
|
+
* touching `process.env` or reading the YAML file themselves. The
|
|
6
|
+
* service intentionally has no plugin-specific types; everything is
|
|
7
|
+
* keyed by dotted path strings (e.g. `"core.postgresPassword"`,
|
|
8
|
+
* `"telegram.botToken"`) and validated to a primitive shape at the
|
|
9
|
+
* call site.
|
|
10
|
+
*
|
|
11
|
+
* Required-vs-optional semantics live in the call signature, not in
|
|
12
|
+
* the schema: omitting `defaultValue` makes the read throw on missing
|
|
13
|
+
* or wrong-shaped data, while supplying one widens the return type to
|
|
14
|
+
* include the default (so plugins can pass `null` for fields they
|
|
15
|
+
* tolerate being absent).
|
|
16
|
+
*
|
|
17
|
+
* The platform-level minimum (the keys without which the daemon can't
|
|
18
|
+
* start) is enforced by a separate `ConfigLinter` on the host side at
|
|
19
|
+
* boot. Plugin-side keys are not policed by the platform — plugins own
|
|
20
|
+
* the validation of their own subtrees.
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* Fallback for `core.writablePaths` when `config.yml` doesn't set it:
|
|
24
|
+
* the workspace globs the agent may write to, which are never handlers.
|
|
25
|
+
*/
|
|
26
|
+
export const DEFAULT_WRITABLE_PATHS = ["wiki/**", "files/**"];
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared bridge network all Familiar containers join. Owned
|
|
3
|
+
* by the daemon (see `commands/Start.ts`); created once via
|
|
4
|
+
* {@link ensureNetwork} before any container is started.
|
|
5
|
+
*/
|
|
6
|
+
export declare const SHARED_NETWORK_NAME = "familiar-net";
|
|
7
|
+
/**
|
|
8
|
+
* Internal (egress-less) bridge network the locked-down agent container
|
|
9
|
+
* joins. Created with `--internal` so the agent has no route to the
|
|
10
|
+
* internet; it reaches postgres (dual-homed onto this net) and the host
|
|
11
|
+
* bastion (via the `familiar-bastion-bridge` socat sidecar, also on this
|
|
12
|
+
* net) by container name. Owned by the daemon, created via
|
|
13
|
+
* {@link ensureNetwork} with `{ internal: true }`.
|
|
14
|
+
*/
|
|
15
|
+
export declare const ISOLATED_NETWORK_NAME = "familiar-isolated";
|
|
16
|
+
/** Options shared by {@link dockerExec}. */
|
|
17
|
+
export interface DockerExecOptions {
|
|
18
|
+
/** If true, non-zero exit codes resolve instead of throwing. */
|
|
19
|
+
readonly allowFailure?: boolean;
|
|
20
|
+
}
|
|
21
|
+
/** Output of {@link dockerCapture}. */
|
|
22
|
+
export interface DockerCaptureResult {
|
|
23
|
+
readonly code: number | null;
|
|
24
|
+
readonly stdout: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Run a docker CLI command and discard its stdio. Resolves on exit
|
|
28
|
+
* code 0 (or always when `allowFailure` is set); rejects otherwise.
|
|
29
|
+
*/
|
|
30
|
+
export declare function dockerExec(args: readonly string[], options?: DockerExecOptions): Promise<void>;
|
|
31
|
+
/**
|
|
32
|
+
* Run a docker CLI command and capture stdout. Never throws on non-zero
|
|
33
|
+
* exit; callers inspect the returned `code`.
|
|
34
|
+
*/
|
|
35
|
+
export declare function dockerCapture(args: readonly string[]): Promise<DockerCaptureResult>;
|
|
36
|
+
/** Whether a container with the given name is currently running. */
|
|
37
|
+
export declare function isContainerRunning(name: string): Promise<boolean>;
|
|
38
|
+
/** Whether a Docker network with the given name already exists. */
|
|
39
|
+
export declare function isNetworkPresent(name: string): Promise<boolean>;
|
|
40
|
+
/**
|
|
41
|
+
* Whether a Docker image with the given reference is present in the
|
|
42
|
+
* local image store. Used by the pull-mode image lifecycle to avoid
|
|
43
|
+
* re-pulling a version-pinned image that's already local.
|
|
44
|
+
*
|
|
45
|
+
* @param ref Image reference (tag or registry-qualified `name:tag`).
|
|
46
|
+
* @returns true if `docker image inspect` succeeds.
|
|
47
|
+
*/
|
|
48
|
+
export declare function isImagePresent(ref: string): Promise<boolean>;
|
|
49
|
+
/** Options for {@link buildNetworkCreateArgs} / {@link ensureNetwork}. */
|
|
50
|
+
export interface NetworkCreateOptions {
|
|
51
|
+
/**
|
|
52
|
+
* Create the network with `--internal` so containers on it have no
|
|
53
|
+
* egress to the host or the internet — only to other containers on
|
|
54
|
+
* the same network. Used for the agent's `familiar-isolated` net.
|
|
55
|
+
*/
|
|
56
|
+
readonly internal?: boolean;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Build the `docker network create` argument vector. Pure (no side
|
|
60
|
+
* effects) so it can be unit-tested without a daemon.
|
|
61
|
+
*
|
|
62
|
+
* @param name Network name to create.
|
|
63
|
+
* @param options Network creation flags (e.g. `internal`).
|
|
64
|
+
* @returns The full docker CLI argv, e.g. `["network", "create", "--internal", "familiar-isolated"]`.
|
|
65
|
+
*/
|
|
66
|
+
export declare function buildNetworkCreateArgs(name: string, options?: NetworkCreateOptions): string[];
|
|
67
|
+
/**
|
|
68
|
+
* Create the named bridge network if it doesn't already exist. Idempotent.
|
|
69
|
+
*
|
|
70
|
+
* @param name Network name to ensure.
|
|
71
|
+
* @param options Network creation flags applied only on first creation
|
|
72
|
+
* (an existing network is left as-is).
|
|
73
|
+
*/
|
|
74
|
+
export declare function ensureNetwork(name: string, options?: NetworkCreateOptions): Promise<void>;
|
|
75
|
+
/**
|
|
76
|
+
* Attach an already-running container to an additional network by name.
|
|
77
|
+
* Callers recreate their container on every `start()` (via
|
|
78
|
+
* {@link removeContainer} first), so the attachment is always fresh —
|
|
79
|
+
* this throws on failure rather than swallowing it, so a genuine
|
|
80
|
+
* problem (missing network, missing container) surfaces instead of
|
|
81
|
+
* silently leaving the container unable to reach its peer.
|
|
82
|
+
*
|
|
83
|
+
* @param network Network to connect the container to.
|
|
84
|
+
* @param container Name of the running container to attach.
|
|
85
|
+
* @throws If the docker command fails (network/container missing, etc.).
|
|
86
|
+
*/
|
|
87
|
+
export declare function connectNetwork(network: string, container: string): Promise<void>;
|
|
88
|
+
/**
|
|
89
|
+
* Force-remove a container by name. Default `allowFailure: true` because
|
|
90
|
+
* "remove if it exists" is the common idempotent-cleanup intent.
|
|
91
|
+
*/
|
|
92
|
+
export declare function removeContainer(name: string, options?: DockerExecOptions): Promise<void>;
|
|
93
|
+
/** Send `docker stop` to a container. Ignores already-stopped/missing containers. */
|
|
94
|
+
export declare function stopContainer(name: string): Promise<void>;
|
|
95
|
+
/**
|
|
96
|
+
* Run a docker CLI command with the parent process's stdin/stdout/
|
|
97
|
+
* stderr inherited, blocking until the child exits. Used by
|
|
98
|
+
* subcommands that hand the user's terminal to a foreground docker
|
|
99
|
+
* process (`familiar psql`'s psql shell, `familiar mcp call`'s
|
|
100
|
+
* one-shot CLI invocations against an MCP runtime container).
|
|
101
|
+
*
|
|
102
|
+
* Returns the child's exit status — `null`-on-signal collapses to
|
|
103
|
+
* 1 so the caller's `process.exit(...)` always sees a usable
|
|
104
|
+
* number. A failure to launch docker itself (e.g. binary missing)
|
|
105
|
+
* surfaces as a thrown {@link Error} the caller can format.
|
|
106
|
+
*
|
|
107
|
+
* @throws If the docker binary couldn't be spawned at all.
|
|
108
|
+
*/
|
|
109
|
+
export declare function dockerInteractive(args: readonly string[]): number;
|
|
110
|
+
/**
|
|
111
|
+
* On Linux, `host.docker.internal` isn't built-in and must be mapped to
|
|
112
|
+
* the host gateway explicitly. macOS and Windows resolve it natively.
|
|
113
|
+
*
|
|
114
|
+
* Used by sidecars that dial a host-side listener (the bastion bridge,
|
|
115
|
+
* the tailscale plugin's serve proxy to the webserver).
|
|
116
|
+
*
|
|
117
|
+
* @returns Extra `docker run` args, or an empty array on macOS/Windows.
|
|
118
|
+
*/
|
|
119
|
+
export declare function hostGatewayArgs(): string[];
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
/**
|
|
4
|
+
* Shared bridge network all Familiar containers join. Owned
|
|
5
|
+
* by the daemon (see `commands/Start.ts`); created once via
|
|
6
|
+
* {@link ensureNetwork} before any container is started.
|
|
7
|
+
*/
|
|
8
|
+
export const SHARED_NETWORK_NAME = "familiar-net";
|
|
9
|
+
/**
|
|
10
|
+
* Internal (egress-less) bridge network the locked-down agent container
|
|
11
|
+
* joins. Created with `--internal` so the agent has no route to the
|
|
12
|
+
* internet; it reaches postgres (dual-homed onto this net) and the host
|
|
13
|
+
* bastion (via the `familiar-bastion-bridge` socat sidecar, also on this
|
|
14
|
+
* net) by container name. Owned by the daemon, created via
|
|
15
|
+
* {@link ensureNetwork} with `{ internal: true }`.
|
|
16
|
+
*/
|
|
17
|
+
export const ISOLATED_NETWORK_NAME = "familiar-isolated";
|
|
18
|
+
/**
|
|
19
|
+
* Run a docker CLI command and discard its stdio. Resolves on exit
|
|
20
|
+
* code 0 (or always when `allowFailure` is set); rejects otherwise.
|
|
21
|
+
*/
|
|
22
|
+
export function dockerExec(args, options = {}) {
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
const proc = spawn("docker", [...args], { stdio: ["ignore", "ignore", "pipe"] });
|
|
25
|
+
let stderr = "";
|
|
26
|
+
proc.stderr.on("data", (chunk) => {
|
|
27
|
+
stderr += chunk.toString();
|
|
28
|
+
});
|
|
29
|
+
proc.on("close", (code) => {
|
|
30
|
+
if (code === 0 || options.allowFailure) {
|
|
31
|
+
resolve();
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const trimmed = stderr.trim();
|
|
35
|
+
const suffix = trimmed.length > 0 ? `: ${trimmed}` : "";
|
|
36
|
+
reject(new Error(`docker ${args.join(" ")} exited with code ${code}${suffix}`));
|
|
37
|
+
});
|
|
38
|
+
proc.on("error", reject);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Run a docker CLI command and capture stdout. Never throws on non-zero
|
|
43
|
+
* exit; callers inspect the returned `code`.
|
|
44
|
+
*/
|
|
45
|
+
export function dockerCapture(args) {
|
|
46
|
+
return new Promise((resolve, reject) => {
|
|
47
|
+
const proc = spawn("docker", [...args], { stdio: ["ignore", "pipe", "ignore"] });
|
|
48
|
+
let stdout = "";
|
|
49
|
+
proc.stdout.on("data", (chunk) => {
|
|
50
|
+
stdout += chunk.toString();
|
|
51
|
+
});
|
|
52
|
+
proc.on("close", (code) => {
|
|
53
|
+
resolve({ code, stdout });
|
|
54
|
+
});
|
|
55
|
+
proc.on("error", reject);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
/** Whether a container with the given name is currently running. */
|
|
59
|
+
export async function isContainerRunning(name) {
|
|
60
|
+
const { code, stdout } = await dockerCapture([
|
|
61
|
+
"container",
|
|
62
|
+
"inspect",
|
|
63
|
+
"-f",
|
|
64
|
+
"{{.State.Running}}",
|
|
65
|
+
name,
|
|
66
|
+
]);
|
|
67
|
+
return code === 0 && stdout.trim() === "true";
|
|
68
|
+
}
|
|
69
|
+
/** Whether a Docker network with the given name already exists. */
|
|
70
|
+
export async function isNetworkPresent(name) {
|
|
71
|
+
const { code } = await dockerCapture(["network", "inspect", name]);
|
|
72
|
+
return code === 0;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Whether a Docker image with the given reference is present in the
|
|
76
|
+
* local image store. Used by the pull-mode image lifecycle to avoid
|
|
77
|
+
* re-pulling a version-pinned image that's already local.
|
|
78
|
+
*
|
|
79
|
+
* @param ref Image reference (tag or registry-qualified `name:tag`).
|
|
80
|
+
* @returns true if `docker image inspect` succeeds.
|
|
81
|
+
*/
|
|
82
|
+
export async function isImagePresent(ref) {
|
|
83
|
+
const { code } = await dockerCapture(["image", "inspect", ref]);
|
|
84
|
+
return code === 0;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Build the `docker network create` argument vector. Pure (no side
|
|
88
|
+
* effects) so it can be unit-tested without a daemon.
|
|
89
|
+
*
|
|
90
|
+
* @param name Network name to create.
|
|
91
|
+
* @param options Network creation flags (e.g. `internal`).
|
|
92
|
+
* @returns The full docker CLI argv, e.g. `["network", "create", "--internal", "familiar-isolated"]`.
|
|
93
|
+
*/
|
|
94
|
+
export function buildNetworkCreateArgs(name, options = {}) {
|
|
95
|
+
const args = ["network", "create"];
|
|
96
|
+
if (options.internal) {
|
|
97
|
+
args.push("--internal");
|
|
98
|
+
}
|
|
99
|
+
args.push(name);
|
|
100
|
+
return args;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Create the named bridge network if it doesn't already exist. Idempotent.
|
|
104
|
+
*
|
|
105
|
+
* @param name Network name to ensure.
|
|
106
|
+
* @param options Network creation flags applied only on first creation
|
|
107
|
+
* (an existing network is left as-is).
|
|
108
|
+
*/
|
|
109
|
+
export async function ensureNetwork(name, options = {}) {
|
|
110
|
+
if (!(await isNetworkPresent(name))) {
|
|
111
|
+
await dockerExec(buildNetworkCreateArgs(name, options));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Attach an already-running container to an additional network by name.
|
|
116
|
+
* Callers recreate their container on every `start()` (via
|
|
117
|
+
* {@link removeContainer} first), so the attachment is always fresh —
|
|
118
|
+
* this throws on failure rather than swallowing it, so a genuine
|
|
119
|
+
* problem (missing network, missing container) surfaces instead of
|
|
120
|
+
* silently leaving the container unable to reach its peer.
|
|
121
|
+
*
|
|
122
|
+
* @param network Network to connect the container to.
|
|
123
|
+
* @param container Name of the running container to attach.
|
|
124
|
+
* @throws If the docker command fails (network/container missing, etc.).
|
|
125
|
+
*/
|
|
126
|
+
export function connectNetwork(network, container) {
|
|
127
|
+
return dockerExec(["network", "connect", network, container]);
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Force-remove a container by name. Default `allowFailure: true` because
|
|
131
|
+
* "remove if it exists" is the common idempotent-cleanup intent.
|
|
132
|
+
*/
|
|
133
|
+
export function removeContainer(name, options = { allowFailure: true }) {
|
|
134
|
+
return dockerExec(["rm", "-f", name], options);
|
|
135
|
+
}
|
|
136
|
+
/** Send `docker stop` to a container. Ignores already-stopped/missing containers. */
|
|
137
|
+
export function stopContainer(name) {
|
|
138
|
+
return dockerExec(["stop", name], { allowFailure: true });
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Run a docker CLI command with the parent process's stdin/stdout/
|
|
142
|
+
* stderr inherited, blocking until the child exits. Used by
|
|
143
|
+
* subcommands that hand the user's terminal to a foreground docker
|
|
144
|
+
* process (`familiar psql`'s psql shell, `familiar mcp call`'s
|
|
145
|
+
* one-shot CLI invocations against an MCP runtime container).
|
|
146
|
+
*
|
|
147
|
+
* Returns the child's exit status — `null`-on-signal collapses to
|
|
148
|
+
* 1 so the caller's `process.exit(...)` always sees a usable
|
|
149
|
+
* number. A failure to launch docker itself (e.g. binary missing)
|
|
150
|
+
* surfaces as a thrown {@link Error} the caller can format.
|
|
151
|
+
*
|
|
152
|
+
* @throws If the docker binary couldn't be spawned at all.
|
|
153
|
+
*/
|
|
154
|
+
export function dockerInteractive(args) {
|
|
155
|
+
const result = spawnSync("docker", [...args], { stdio: "inherit" });
|
|
156
|
+
if (result.error) {
|
|
157
|
+
throw new Error(`failed to launch docker: ${result.error.message}`);
|
|
158
|
+
}
|
|
159
|
+
return result.status ?? 1;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* On Linux, `host.docker.internal` isn't built-in and must be mapped to
|
|
163
|
+
* the host gateway explicitly. macOS and Windows resolve it natively.
|
|
164
|
+
*
|
|
165
|
+
* Used by sidecars that dial a host-side listener (the bastion bridge,
|
|
166
|
+
* the tailscale plugin's serve proxy to the webserver).
|
|
167
|
+
*
|
|
168
|
+
* @returns Extra `docker run` args, or an empty array on macOS/Windows.
|
|
169
|
+
*/
|
|
170
|
+
export function hostGatewayArgs() {
|
|
171
|
+
if (os.platform() === "linux") {
|
|
172
|
+
return ["--add-host=host.docker.internal:host-gateway"];
|
|
173
|
+
}
|
|
174
|
+
return [];
|
|
175
|
+
}
|
package/build/Event.d.ts
CHANGED
|
@@ -41,9 +41,9 @@ export interface EventRow {
|
|
|
41
41
|
*/
|
|
42
42
|
readonly id: string;
|
|
43
43
|
/**
|
|
44
|
-
* Event topic, matching
|
|
45
|
-
* `chat:telegram:group:reaction`).
|
|
46
|
-
*
|
|
44
|
+
* Event topic, matching `TOPIC_PATTERN` (e.g. `mail`, `chat:whatsapp`,
|
|
45
|
+
* `chat:telegram:group:reaction`). The topic is the handler's whole
|
|
46
|
+
* address: `a:b:c` resolves to `a/b/c.md` (or `a/b/c/index.md`).
|
|
47
47
|
*/
|
|
48
48
|
readonly topic: string;
|
|
49
49
|
/**
|
|
@@ -94,12 +94,12 @@ export interface EventRow {
|
|
|
94
94
|
*/
|
|
95
95
|
readonly prompt: string;
|
|
96
96
|
/**
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
97
|
+
* `true` when the topic must name an existing handler file exactly;
|
|
98
|
+
* `false` when handler resolution may fall back up the topic chain.
|
|
99
|
+
* Set by the emitter via {@link NewEvent.isExactTopic}; never
|
|
100
|
+
* mutated after insert.
|
|
101
101
|
*/
|
|
102
|
-
readonly
|
|
102
|
+
readonly isExactTopic: boolean;
|
|
103
103
|
/**
|
|
104
104
|
* `true` when this event was emitted by a trusted user-input source
|
|
105
105
|
* (the operator at the local terminal via cli-chat, the operator on
|
|
@@ -236,24 +236,24 @@ export interface NewEvent {
|
|
|
236
236
|
*
|
|
237
237
|
* Use this for plugin emitters that want a chatmessage text
|
|
238
238
|
* different from `prompt` (e.g. cli-chat persists the slash-
|
|
239
|
-
* prefixed `/topic/
|
|
239
|
+
* prefixed `/topic/sub …` line while the handler sees only
|
|
240
240
|
* the trailing prompt text). When unset and `isChat: true`, the
|
|
241
241
|
* existing path inserts `prompt` itself.
|
|
242
242
|
*/
|
|
243
243
|
readonly userChatMessage?: string;
|
|
244
244
|
/**
|
|
245
|
-
*
|
|
246
|
-
*
|
|
247
|
-
*
|
|
248
|
-
*
|
|
245
|
+
* When `true`, the topic addresses one handler file exactly
|
|
246
|
+
* (`a:b:c` → `a/b/c.md` or `a/b/c/index.md`) and the run fails if
|
|
247
|
+
* it is missing. Default `false`: world events fall back up the
|
|
248
|
+
* topic chain (`a:b`, then `a`) to the most specific existing
|
|
249
|
+
* handler. Set it for emissions that name a handler rather than
|
|
250
|
+
* report a world change — cron firings, scheduled wake-ups,
|
|
251
|
+
* memory's `skills:memory:save`, cli-chat slash commands.
|
|
249
252
|
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
* time if no such file exists — there is no shape validation
|
|
253
|
-
* here, mirroring how `topic` is only regex-checked, not
|
|
254
|
-
* existence-checked, at emit time.
|
|
253
|
+
* Like `topic` itself, existence is not checked at emit time; the
|
|
254
|
+
* agentrun fails loud at handler-load time.
|
|
255
255
|
*/
|
|
256
|
-
readonly
|
|
256
|
+
readonly isExactTopic?: boolean;
|
|
257
257
|
/**
|
|
258
258
|
* Auxiliary files to stage at `/scratch/<event-id>/<name>` for this
|
|
259
259
|
* event. Visible to the agent and to every MCP container under the
|
package/build/EventBus.d.ts
CHANGED
|
@@ -116,7 +116,7 @@ export declare class EventBus {
|
|
|
116
116
|
*/
|
|
117
117
|
listLatest(limit: number): Promise<EventRow[]>;
|
|
118
118
|
/**
|
|
119
|
-
* Fetch the most recently created N event rows whose topic,
|
|
119
|
+
* Fetch the most recently created N event rows whose topic,
|
|
120
120
|
* prompt, or payload (cast to text) case-insensitively contains `needle`.
|
|
121
121
|
* Used by `familiar events list <search>` so the operator can grep the
|
|
122
122
|
* recent bus without piping through `grep`.
|
|
@@ -138,7 +138,7 @@ export declare class EventBus {
|
|
|
138
138
|
* pair can't express. Both of those stay as the CLI's call sites.
|
|
139
139
|
*
|
|
140
140
|
* Substring match (when `search` is set) is the same ILIKE-against-
|
|
141
|
-
* `(topic,
|
|
141
|
+
* `(topic, prompt, payload::text)` predicate
|
|
142
142
|
* {@link searchLatest} uses. `dateFrom` is inclusive, `dateTo` is
|
|
143
143
|
* exclusive (`created_at >= $from AND created_at < $to`), so the
|
|
144
144
|
* agent can pass two midnights to get a single local day without
|
package/build/EventBus.js
CHANGED
|
@@ -139,7 +139,7 @@ export class EventBus {
|
|
|
139
139
|
const result = await client.query(`INSERT INTO events
|
|
140
140
|
(topic, payload, priority, state, idempotency_key,
|
|
141
141
|
is_chat, preferred_chat_channel_id, prompt, privileged,
|
|
142
|
-
|
|
142
|
+
is_exact_topic, output_chat_on_failure)
|
|
143
143
|
VALUES ($1, $2::jsonb, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
|
144
144
|
RETURNING *`, [
|
|
145
145
|
event.topic,
|
|
@@ -151,7 +151,7 @@ export class EventBus {
|
|
|
151
151
|
event.preferredChatChannelId ?? null,
|
|
152
152
|
prompt,
|
|
153
153
|
event.privileged ?? false,
|
|
154
|
-
event.
|
|
154
|
+
event.isExactTopic ?? false,
|
|
155
155
|
event.outputChatOnFailure ?? false,
|
|
156
156
|
]);
|
|
157
157
|
const row = mapRow(result.rows[0]);
|
|
@@ -242,7 +242,7 @@ export class EventBus {
|
|
|
242
242
|
return result.rows.map(mapRow);
|
|
243
243
|
}
|
|
244
244
|
/**
|
|
245
|
-
* Fetch the most recently created N event rows whose topic,
|
|
245
|
+
* Fetch the most recently created N event rows whose topic,
|
|
246
246
|
* prompt, or payload (cast to text) case-insensitively contains `needle`.
|
|
247
247
|
* Used by `familiar events list <search>` so the operator can grep the
|
|
248
248
|
* recent bus without piping through `grep`.
|
|
@@ -259,7 +259,6 @@ export class EventBus {
|
|
|
259
259
|
const pattern = `%${needle.replace(/[\\%_]/g, (c) => `\\${c}`)}%`;
|
|
260
260
|
const result = await this.connection.getPool().query(`SELECT * FROM events
|
|
261
261
|
WHERE topic ILIKE $2
|
|
262
|
-
OR coalesce(start_handler, '') ILIKE $2
|
|
263
262
|
OR coalesce(prompt, '') ILIKE $2
|
|
264
263
|
OR payload::text ILIKE $2
|
|
265
264
|
ORDER BY created_at DESC, id DESC
|
|
@@ -274,7 +273,7 @@ export class EventBus {
|
|
|
274
273
|
* pair can't express. Both of those stay as the CLI's call sites.
|
|
275
274
|
*
|
|
276
275
|
* Substring match (when `search` is set) is the same ILIKE-against-
|
|
277
|
-
* `(topic,
|
|
276
|
+
* `(topic, prompt, payload::text)` predicate
|
|
278
277
|
* {@link searchLatest} uses. `dateFrom` is inclusive, `dateTo` is
|
|
279
278
|
* exclusive (`created_at >= $from AND created_at < $to`), so the
|
|
280
279
|
* agent can pass two midnights to get a single local day without
|
|
@@ -286,8 +285,7 @@ export class EventBus {
|
|
|
286
285
|
let n = 1;
|
|
287
286
|
if (filter.search !== undefined && filter.search.length > 0) {
|
|
288
287
|
const pattern = `%${filter.search.replace(/[\\%_]/g, (c) => `\\${c}`)}%`;
|
|
289
|
-
conditions.push(`(topic ILIKE $${n} OR coalesce(
|
|
290
|
-
`OR coalesce(prompt, '') ILIKE $${n} OR payload::text ILIKE $${n})`);
|
|
288
|
+
conditions.push(`(topic ILIKE $${n} OR coalesce(prompt, '') ILIKE $${n} OR payload::text ILIKE $${n})`);
|
|
291
289
|
values.push(pattern);
|
|
292
290
|
n++;
|
|
293
291
|
}
|
|
@@ -473,7 +471,7 @@ function mapRow(raw) {
|
|
|
473
471
|
isChat: raw.is_chat,
|
|
474
472
|
preferredChatChannelId: raw.preferred_chat_channel_id,
|
|
475
473
|
prompt: raw.prompt ?? "",
|
|
476
|
-
|
|
474
|
+
isExactTopic: raw.is_exact_topic,
|
|
477
475
|
privileged: raw.privileged,
|
|
478
476
|
outputChatOnFailure: raw.output_chat_on_failure,
|
|
479
477
|
createdAt: raw.created_at,
|
|
@@ -1,31 +1,35 @@
|
|
|
1
|
+
import { type HandlerResolution, type HandlerResolutionOptions } from "./HandlerResolution.js";
|
|
1
2
|
/**
|
|
2
3
|
* One handler entry surfaced by {@link HandlerCatalog.list}.
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* The topic is the handler's whole address (`chat:telegram` for both
|
|
6
|
+
* `chat/telegram.md` and `chat/telegram/index.md`). `slashPath` is the
|
|
7
|
+
* user-facing form the cli-chat prompt accepts as a direct handler call
|
|
8
|
+
* (e.g. `/grocery/fruits/order`).
|
|
8
9
|
*/
|
|
9
10
|
export interface HandlerPath {
|
|
10
11
|
/** Colon-separated topic, e.g. `chat:telegram`. */
|
|
11
12
|
readonly topic: string;
|
|
12
|
-
/** Handler basename without `.md`, e.g. `index`, `analyze`. */
|
|
13
|
-
readonly handler: string;
|
|
14
13
|
/** Absolute path on disk of the handler file. */
|
|
15
14
|
readonly absolutePath: string;
|
|
16
|
-
/** Workspace-relative posix path, e.g. `chat/telegram
|
|
15
|
+
/** Workspace-relative posix path, e.g. `chat/telegram.md`. */
|
|
17
16
|
readonly relativePath: string;
|
|
18
|
-
/** User-facing slash form, e.g. `/chat/telegram
|
|
17
|
+
/** User-facing slash form of the topic, e.g. `/chat/telegram`. */
|
|
19
18
|
readonly slashPath: string;
|
|
20
19
|
}
|
|
20
|
+
/** A {@link HandlerResolution} plus the winning file's absolute path. */
|
|
21
|
+
export interface ResolvedHandler extends HandlerResolution {
|
|
22
|
+
/** Absolute path on disk of the winning handler file. */
|
|
23
|
+
readonly absolutePath: string;
|
|
24
|
+
}
|
|
21
25
|
/**
|
|
22
26
|
* Read-only view over the `workspace/` directory's handler files.
|
|
23
27
|
*
|
|
24
|
-
*
|
|
25
|
-
* `
|
|
28
|
+
* Shares the resolution rules of `container/src/HandlerFile.ts` (both
|
|
29
|
+
* build on `HandlerResolution.ts`) so host-side code (CLI, cron
|
|
26
30
|
* scheduler introspection, tab-completion) can answer "does this
|
|
27
31
|
* handler exist?" and "list every handler" without depending on
|
|
28
|
-
* container internals
|
|
32
|
+
* container internals.
|
|
29
33
|
*
|
|
30
34
|
* The catalog is stateless on the disk — every method re-scans —
|
|
31
35
|
* so live workspace edits are reflected without a daemon restart.
|
|
@@ -46,12 +50,14 @@ export declare class HandlerCatalog {
|
|
|
46
50
|
constructor(workspaceDir: string, writablePathGlobs?: readonly string[]);
|
|
47
51
|
/**
|
|
48
52
|
* Walk `workspace/` and return one {@link HandlerPath} per
|
|
49
|
-
*
|
|
50
|
-
* (`PROMPT.md`, `SOUL.md`, … — root files have no topic, so they
|
|
51
|
-
* never handlers)
|
|
52
|
-
*
|
|
53
|
-
* `
|
|
54
|
-
*
|
|
53
|
+
* addressable handler file. Excludes every file at the workspace
|
|
54
|
+
* root (`PROMPT.md`, `SOUL.md`, … — root files have no topic, so they
|
|
55
|
+
* are never handlers), the `skills/` subtree, writable paths, file
|
|
56
|
+
* names that don't form a valid topic, and a folder-form
|
|
57
|
+
* `x/index.md` shadowed by its `x.md` twin. Every other `.md` file
|
|
58
|
+
* under a topic directory is included, even if it's a knowledge file
|
|
59
|
+
* (e.g. `people/anna.md`) — `HandlerCatalog` does not police what is
|
|
60
|
+
* or isn't a "proper" handler; that's a handler-loading concern.
|
|
55
61
|
*
|
|
56
62
|
* Sorted by `slashPath` for stable display ordering in
|
|
57
63
|
* tab-completion. Returns an empty array when the workspace
|
|
@@ -59,17 +65,16 @@ export declare class HandlerCatalog {
|
|
|
59
65
|
*/
|
|
60
66
|
list(): Promise<readonly HandlerPath[]>;
|
|
61
67
|
/**
|
|
62
|
-
* Resolve a handler
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
68
|
+
* Resolve a topic to the one handler file that runs for it, with
|
|
69
|
+
* the same rules as the container's `HandlerFile.load`: file form
|
|
70
|
+
* `a/b/c.md` before folder form `a/b/c/index.md`, and — unless
|
|
71
|
+
* `isExactTopic` — falling back up the topic chain.
|
|
66
72
|
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
* file
|
|
71
|
-
*
|
|
72
|
-
* want answered.
|
|
73
|
+
* @param topic Colon-separated topic, e.g. `chat:telegram`.
|
|
74
|
+
* @param options See {@link HandlerResolutionOptions}.
|
|
75
|
+
* @returns The resolution, or `null` when nothing matches or the
|
|
76
|
+
* winning file lives under a writable path (the container refuses
|
|
77
|
+
* to run it, so it does not resolve).
|
|
73
78
|
*/
|
|
74
|
-
resolve(topic: string,
|
|
79
|
+
resolve(topic: string, options: HandlerResolutionOptions): Promise<ResolvedHandler | null>;
|
|
75
80
|
}
|