@jini-ai/daemon 0.3.8 → 0.5.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/CHANGELOG.md +119 -0
- package/README.md +50 -8
- package/dist/agent-executor/contracts.d.ts +736 -0
- package/dist/agent-executor/contracts.js +28 -0
- package/dist/agent-executor/event-translation.d.ts +87 -0
- package/dist/agent-executor/event-translation.js +230 -0
- package/dist/agent-executor/events.d.ts +190 -0
- package/dist/agent-executor/events.js +848 -0
- package/dist/agent-executor/handoff.d.ts +18 -0
- package/dist/agent-executor/handoff.js +16 -0
- package/dist/agent-executor/index.d.ts +98 -0
- package/dist/agent-executor/index.js +98 -0
- package/dist/agent-executor/launch.d.ts +375 -0
- package/dist/agent-executor/launch.js +1001 -0
- package/dist/agent-executor/mcp-bridge.d.ts +338 -0
- package/dist/agent-executor/mcp-bridge.js +835 -0
- package/dist/agent-executor/values.d.ts +5 -0
- package/dist/agent-executor/values.js +13 -0
- package/dist/agent-executor.d.ts +375 -176
- package/dist/agent-executor.js +388 -335
- package/dist/artifacts/index.d.ts +14 -0
- package/dist/artifacts/index.js +14 -0
- package/dist/artifacts/manifest.d.ts +90 -0
- package/dist/artifacts/manifest.js +229 -0
- package/dist/artifacts/publication-guard.d.ts +28 -0
- package/dist/artifacts/publication-guard.js +77 -0
- package/dist/artifacts/runtime-compat.d.ts +37 -0
- package/dist/artifacts/runtime-compat.js +33 -0
- package/dist/artifacts/store.d.ts +85 -0
- package/dist/artifacts/store.js +92 -0
- package/dist/artifacts/stub-guard.d.ts +73 -0
- package/dist/artifacts/stub-guard.js +198 -0
- package/dist/artifacts/text-suppression.d.ts +68 -0
- package/dist/artifacts/text-suppression.js +184 -0
- package/dist/close-status.d.ts +11 -8
- package/dist/close-status.js +31 -19
- package/dist/continuation/continuation-transport.d.ts +3 -1
- package/dist/continuation/continuation-transport.js +1 -1
- package/dist/continuation/journal.d.ts +12 -5
- package/dist/continuation/journal.js +4 -4
- package/dist/continuation/run-scoped-context-store.d.ts +13 -4
- package/dist/continuation/run-scoped-context-store.js +6 -6
- package/dist/continuation/run-start-handler.d.ts +1 -1
- package/dist/continuation/run-start-handler.js +3 -16
- package/dist/delegated-tool-bridge.d.ts +7 -3
- package/dist/delegated-tool-bridge.js +32 -31
- package/dist/event-log.d.ts +7 -5
- package/dist/event-log.js +12 -11
- package/dist/frontend-capability-tools.d.ts +1 -1
- package/dist/frontend-capability-tools.js +3 -2
- package/dist/frontend-session-registry.d.ts +32 -9
- package/dist/frontend-session-registry.js +12 -11
- package/dist/http/active-context.d.ts +105 -0
- package/dist/http/active-context.js +67 -0
- package/dist/http/agents.d.ts +68 -0
- package/dist/http/agents.js +12 -0
- package/dist/http/attachments.d.ts +618 -0
- package/dist/http/attachments.js +1034 -0
- package/dist/http/cancel-owned-runs.d.ts +49 -0
- package/dist/http/cancel-owned-runs.js +35 -0
- package/dist/http/component-catalog.d.ts +84 -0
- package/dist/http/component-catalog.js +64 -0
- package/dist/http/connectors.d.ts +209 -0
- package/dist/http/connectors.js +409 -0
- package/dist/http/daemon-status.d.ts +76 -0
- package/dist/http/daemon-status.js +55 -0
- package/dist/http/db-ops.d.ts +123 -0
- package/dist/http/db-ops.js +189 -0
- package/dist/http/delegated-tools.d.ts +113 -0
- package/dist/http/delegated-tools.js +230 -0
- package/dist/http/frontend-control.d.ts +136 -0
- package/dist/http/frontend-control.js +79 -0
- package/dist/http/frontend-sessions.d.ts +110 -0
- package/dist/http/frontend-sessions.js +180 -0
- package/dist/http/health.d.ts +95 -0
- package/dist/http/health.js +40 -0
- package/dist/http/host-tools.d.ts +214 -0
- package/dist/http/host-tools.js +329 -0
- package/dist/http/index.d.ts +52 -0
- package/dist/http/index.js +27 -0
- package/dist/http/media.d.ts +146 -0
- package/dist/http/media.js +346 -0
- package/dist/http/memory.d.ts +265 -0
- package/dist/http/memory.js +328 -0
- package/dist/http/model-proxy.d.ts +49 -0
- package/dist/http/model-proxy.js +642 -0
- package/dist/http/remote-run-events.d.ts +70 -0
- package/dist/http/remote-run-events.js +224 -0
- package/dist/http/research.d.ts +48 -0
- package/dist/http/research.js +315 -0
- package/dist/http/route-manifest.d.ts +35 -0
- package/dist/http/route-manifest.js +103 -0
- package/dist/http/routines.d.ts +120 -0
- package/dist/http/routines.js +298 -0
- package/dist/http/run-credentials.d.ts +73 -0
- package/dist/http/run-credentials.js +71 -0
- package/dist/http/runs.d.ts +91 -0
- package/dist/http/runs.js +221 -0
- package/dist/http/terminals.d.ts +138 -0
- package/dist/http/terminals.js +325 -0
- package/dist/http/tool-catalog.d.ts +78 -0
- package/dist/http/tool-catalog.js +66 -0
- package/dist/http/workspace-root.d.ts +77 -0
- package/dist/http/workspace-root.js +63 -0
- package/dist/http/xai.d.ts +90 -0
- package/dist/http/xai.js +655 -0
- package/dist/image-prompt-delivery.d.ts +13 -3
- package/dist/image-prompt-delivery.js +5 -5
- package/dist/index.d.ts +556 -4
- package/dist/index.js +556 -4
- package/dist/legacy-data-migration.d.ts +59 -8
- package/dist/legacy-data-migration.js +38 -33
- package/dist/read-only-tools.d.ts +130 -0
- package/dist/read-only-tools.js +115 -0
- package/dist/remote-tool-bridge.d.ts +11 -5
- package/dist/remote-tool-bridge.js +15 -15
- package/dist/routines/index.d.ts +1 -1
- package/dist/routines/index.js +1 -1
- package/dist/routines/routine-store.d.ts +34 -12
- package/dist/routines/routine-store.js +21 -20
- package/dist/routines/schedule.d.ts +48 -10
- package/dist/routines/schedule.js +21 -21
- package/dist/routines/scheduler.d.ts +32 -11
- package/dist/routines/scheduler.js +61 -57
- package/dist/routines/types.d.ts +10 -3
- package/dist/run/core/result.d.ts +6 -2
- package/dist/run/core/result.js +3 -3
- package/dist/run/core/retry.d.ts +17 -4
- package/dist/run/core/retry.js +7 -11
- package/dist/run/diagnostics/diagnostics.d.ts +14 -4
- package/dist/run/diagnostics/diagnostics.js +7 -7
- package/dist/run/index.d.ts +1 -1
- package/dist/run/index.js +1 -1
- package/dist/run-credentials.d.ts +138 -0
- package/dist/run-credentials.js +152 -0
- package/dist/run-lifecycle.d.ts +92 -33
- package/dist/run-lifecycle.js +81 -74
- package/dist/scheduler.d.ts +8 -0
- package/dist/scheduler.js +2 -0
- package/dist/session-coordination.d.ts +115 -0
- package/dist/session-coordination.js +111 -0
- package/dist/store/agent-sessions/errors.d.ts +8 -0
- package/dist/store/agent-sessions/errors.js +10 -0
- package/dist/store/agent-sessions/index.d.ts +12 -0
- package/dist/store/agent-sessions/index.js +19 -0
- package/dist/store/agent-sessions/pglite.d.ts +12 -0
- package/dist/store/agent-sessions/pglite.js +7 -0
- package/dist/store/agent-sessions/ports.d.ts +24 -0
- package/dist/store/agent-sessions/ports.js +2 -0
- package/dist/store/agent-sessions/postgres.d.ts +12 -0
- package/dist/store/agent-sessions/postgres.js +7 -0
- package/dist/store/agent-sessions/schema.d.ts +3 -0
- package/dist/store/agent-sessions/schema.js +23 -0
- package/dist/store/agent-sessions/sql.d.ts +35 -0
- package/dist/store/agent-sessions/sql.js +59 -0
- package/dist/store/agent-sessions/sqlite.d.ts +117 -0
- package/dist/store/agent-sessions/sqlite.js +128 -0
- package/dist/store/event-log/sqlite.d.ts +75 -0
- package/dist/store/event-log/sqlite.js +230 -0
- package/dist/surface-exchanges.d.ts +97 -0
- package/dist/surface-exchanges.js +153 -0
- package/dist/terminal-session.d.ts +43 -11
- package/dist/terminal-session.js +22 -20
- package/dist/tokens.js +4 -4
- package/dist/tool-audit.d.ts +114 -0
- package/dist/tool-audit.js +95 -0
- package/dist/tool-executor.d.ts +22 -7
- package/dist/tool-executor.js +16 -16
- package/dist/tool-result-media.d.ts +3 -1
- package/dist/tool-result-media.js +1 -1
- package/dist/tool-result-surfaces.d.ts +3 -1
- package/dist/tool-result-surfaces.js +1 -1
- package/package.json +127 -9
|
@@ -1,3 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module legacy-data-migration
|
|
3
|
+
*
|
|
4
|
+
* A one-shot, idempotent, host-configured migrator for moving a daemon's
|
|
5
|
+
* data root from one location to another (e.g. a pre-packaging install that
|
|
6
|
+
* ran out of a repo checkout, moved to a per-namespace directory under an
|
|
7
|
+
* OS user-data location). The mechanism — refuse-if-missing-proof,
|
|
8
|
+
* refuse-if-destination-populated, refuse-if-already-migrated, reject
|
|
9
|
+
* symlinks, stage-then-atomically-promote, rollback-on-any-failure — is
|
|
10
|
+
* product-neutral; the *shape* of a data root's payload (which files/dirs
|
|
11
|
+
* constitute "real data", and which single entry proves a directory holds
|
|
12
|
+
* real payload rather than an empty scaffold) is host-supplied via
|
|
13
|
+
* {@link LegacyDataMigrationConfig}, never hardcoded here.
|
|
14
|
+
*
|
|
15
|
+
* Sync by design: intended to run at daemon startup before any database
|
|
16
|
+
* connection opens, so it can't race a concurrent open of the destination.
|
|
17
|
+
*/
|
|
18
|
+
import * as nodeFilesystem from 'node:fs';
|
|
19
|
+
/** Node filesystem ABI, injected so migration can run against a host-owned adapter. */
|
|
20
|
+
export type LegacyFilesystemPort = Pick<typeof nodeFilesystem, 'statSync' | 'lstatSync' | 'existsSync' | 'readdirSync' | 'mkdirSync' | 'cpSync' | 'renameSync' | 'rmSync' | 'writeFileSync'>;
|
|
1
21
|
/** Which files/dirs constitute a data root's migratable payload, host-supplied. */
|
|
2
22
|
export interface LegacyDataMigrationConfig {
|
|
3
23
|
/** Every file/dir under a data root that is part of the migratable payload. */
|
|
@@ -19,8 +39,11 @@ export interface MigrateLegacyDataDirOptions extends LegacyDataMigrationConfig {
|
|
|
19
39
|
dataDir: string;
|
|
20
40
|
/** Optional logger. Defaults to console.log. */
|
|
21
41
|
logger?: {
|
|
22
|
-
info(
|
|
42
|
+
info(args: {
|
|
43
|
+
message: string;
|
|
44
|
+
}): void;
|
|
23
45
|
};
|
|
46
|
+
filesystem?: LegacyFilesystemPort;
|
|
24
47
|
/**
|
|
25
48
|
* Test seam. The default writes the JSON marker with fs.writeFileSync;
|
|
26
49
|
* tests inject a function that throws so the rollback path (which removes
|
|
@@ -29,7 +52,11 @@ export interface MigrateLegacyDataDirOptions extends LegacyDataMigrationConfig {
|
|
|
29
52
|
* pass this.
|
|
30
53
|
* @internal
|
|
31
54
|
*/
|
|
32
|
-
writeMarker?: (
|
|
55
|
+
writeMarker?: (args: {
|
|
56
|
+
dataDir: string;
|
|
57
|
+
legacyDir: string;
|
|
58
|
+
markerFile: string;
|
|
59
|
+
}) => void;
|
|
33
60
|
}
|
|
34
61
|
export type MigrateStatus = 'noop' | 'migrated' | 'skipped';
|
|
35
62
|
export interface MigrateLegacyDataDirResult {
|
|
@@ -45,7 +72,10 @@ export interface MigrateLegacyDataDirResult {
|
|
|
45
72
|
*/
|
|
46
73
|
export declare class LegacyMigrationError extends Error {
|
|
47
74
|
readonly code: string;
|
|
48
|
-
constructor(code
|
|
75
|
+
constructor({ code, message }: {
|
|
76
|
+
readonly code: string;
|
|
77
|
+
readonly message: string;
|
|
78
|
+
});
|
|
49
79
|
}
|
|
50
80
|
/**
|
|
51
81
|
* Returns true when `dataDir` looks like a fresh / never-used data root:
|
|
@@ -58,7 +88,12 @@ export declare class LegacyMigrationError extends Error {
|
|
|
58
88
|
* @param dataDir - The data root to check.
|
|
59
89
|
* @param config - The host's payload/proof-entry configuration.
|
|
60
90
|
*/
|
|
61
|
-
export declare function dataDirIsEmptyOrFresh(dataDir
|
|
91
|
+
export declare function dataDirIsEmptyOrFresh({ dataDir, config }: {
|
|
92
|
+
readonly dataDir: string;
|
|
93
|
+
readonly config: LegacyDataMigrationConfig;
|
|
94
|
+
}, { filesystem: fs }?: {
|
|
95
|
+
filesystem?: LegacyFilesystemPort;
|
|
96
|
+
}): boolean;
|
|
62
97
|
/**
|
|
63
98
|
* Returns true when `legacyDir` contains a payload worth migrating: it
|
|
64
99
|
* exists and its configured `proofEntry` exists as a file.
|
|
@@ -66,7 +101,12 @@ export declare function dataDirIsEmptyOrFresh(dataDir: string, config: LegacyDat
|
|
|
66
101
|
* @param legacyDir - The candidate legacy data directory.
|
|
67
102
|
* @param config - The host's payload/proof-entry configuration.
|
|
68
103
|
*/
|
|
69
|
-
export declare function legacyDirHasPayload(legacyDir
|
|
104
|
+
export declare function legacyDirHasPayload({ legacyDir, config }: {
|
|
105
|
+
readonly legacyDir: string;
|
|
106
|
+
readonly config: LegacyDataMigrationConfig;
|
|
107
|
+
}, { filesystem: fs }?: {
|
|
108
|
+
filesystem?: LegacyFilesystemPort;
|
|
109
|
+
}): boolean;
|
|
70
110
|
/**
|
|
71
111
|
* Returns the list of configured payload entries that already exist under
|
|
72
112
|
* `dataDir`. Migration refuses to merge on top of any of them — a database
|
|
@@ -76,7 +116,12 @@ export declare function legacyDirHasPayload(legacyDir: string, config: LegacyDat
|
|
|
76
116
|
* @param dataDir - The data root to check.
|
|
77
117
|
* @param config - The host's payload/proof-entry configuration.
|
|
78
118
|
*/
|
|
79
|
-
export declare function dataDirHasExistingPayload(dataDir
|
|
119
|
+
export declare function dataDirHasExistingPayload({ dataDir, config }: {
|
|
120
|
+
readonly dataDir: string;
|
|
121
|
+
readonly config: LegacyDataMigrationConfig;
|
|
122
|
+
}, { filesystem: fs }?: {
|
|
123
|
+
filesystem?: LegacyFilesystemPort;
|
|
124
|
+
}): string[];
|
|
80
125
|
/**
|
|
81
126
|
* Move staged payload entries into the final `dataDir`. Each entry is moved
|
|
82
127
|
* with `renameSync` (atomic on the same filesystem); a cross-device rename
|
|
@@ -89,7 +134,13 @@ export declare function dataDirHasExistingPayload(dataDir: string, config: Legac
|
|
|
89
134
|
* @param entries - The payload entries to promote.
|
|
90
135
|
* @returns The entries that were placed into `dataDir` before any failure.
|
|
91
136
|
*/
|
|
92
|
-
export declare function promoteStaged(stagingDir
|
|
137
|
+
export declare function promoteStaged({ stagingDir, dataDir, entries }: {
|
|
138
|
+
readonly stagingDir: string;
|
|
139
|
+
readonly dataDir: string;
|
|
140
|
+
readonly entries: readonly string[];
|
|
141
|
+
}, { filesystem: fs }?: {
|
|
142
|
+
filesystem?: LegacyFilesystemPort;
|
|
143
|
+
}): readonly string[];
|
|
93
144
|
/**
|
|
94
145
|
* One-shot, idempotent legacy data migrator. Synchronous so it can run at
|
|
95
146
|
* module import time before a database connection opens. Throws
|
|
@@ -100,5 +151,5 @@ export declare function promoteStaged(stagingDir: string, dataDir: string, entri
|
|
|
100
151
|
* @returns The migration outcome (`noop` when unconfigured, `skipped` when
|
|
101
152
|
* already migrated, `migrated` with the copied entry list otherwise).
|
|
102
153
|
*/
|
|
103
|
-
export declare function migrateLegacyDataDirSync(
|
|
154
|
+
export declare function migrateLegacyDataDirSync(requiredArgs: Pick<MigrateLegacyDataDirOptions, "legacyDir" | "dataDir" | "payloadEntries" | "proofEntry">, optionalArgs?: Pick<MigrateLegacyDataDirOptions, "logger" | "writeMarker" | "markerFile" | "filesystem">): MigrateLegacyDataDirResult;
|
|
104
155
|
//# sourceMappingURL=legacy-data-migration.d.ts.map
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* Sync by design: intended to run at daemon startup before any database
|
|
16
16
|
* connection opens, so it can't race a concurrent open of the destination.
|
|
17
17
|
*/
|
|
18
|
-
import * as
|
|
18
|
+
import * as nodeFilesystem from 'node:fs';
|
|
19
19
|
import * as path from 'node:path';
|
|
20
20
|
const DEFAULT_MARKER_FILE = '.migrated-from';
|
|
21
21
|
/**
|
|
@@ -26,13 +26,13 @@ const DEFAULT_MARKER_FILE = '.migrated-from';
|
|
|
26
26
|
*/
|
|
27
27
|
export class LegacyMigrationError extends Error {
|
|
28
28
|
code;
|
|
29
|
-
constructor(code, message) {
|
|
29
|
+
constructor({ code, message }) {
|
|
30
30
|
super(message);
|
|
31
31
|
this.code = code;
|
|
32
32
|
this.name = 'LegacyMigrationError';
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
|
-
function isExistingDir(p) {
|
|
35
|
+
function isExistingDir(p, fs) {
|
|
36
36
|
try {
|
|
37
37
|
return fs.statSync(p).isDirectory();
|
|
38
38
|
}
|
|
@@ -40,7 +40,7 @@ function isExistingDir(p) {
|
|
|
40
40
|
return false;
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
|
-
function isExistingFile(p) {
|
|
43
|
+
function isExistingFile(p, fs) {
|
|
44
44
|
try {
|
|
45
45
|
return fs.statSync(p).isFile();
|
|
46
46
|
}
|
|
@@ -59,10 +59,10 @@ function isExistingFile(p) {
|
|
|
59
59
|
* @param dataDir - The data root to check.
|
|
60
60
|
* @param config - The host's payload/proof-entry configuration.
|
|
61
61
|
*/
|
|
62
|
-
export function dataDirIsEmptyOrFresh(dataDir, config) {
|
|
63
|
-
if (!isExistingDir(dataDir))
|
|
62
|
+
export function dataDirIsEmptyOrFresh({ dataDir, config }, { filesystem: fs = nodeFilesystem } = {}) {
|
|
63
|
+
if (!isExistingDir(dataDir, fs))
|
|
64
64
|
return true;
|
|
65
|
-
return !isExistingFile(path.join(dataDir, config.proofEntry));
|
|
65
|
+
return !isExistingFile(path.join(dataDir, config.proofEntry), fs);
|
|
66
66
|
}
|
|
67
67
|
/**
|
|
68
68
|
* Returns true when `legacyDir` contains a payload worth migrating: it
|
|
@@ -71,10 +71,10 @@ export function dataDirIsEmptyOrFresh(dataDir, config) {
|
|
|
71
71
|
* @param legacyDir - The candidate legacy data directory.
|
|
72
72
|
* @param config - The host's payload/proof-entry configuration.
|
|
73
73
|
*/
|
|
74
|
-
export function legacyDirHasPayload(legacyDir, config) {
|
|
75
|
-
if (!isExistingDir(legacyDir))
|
|
74
|
+
export function legacyDirHasPayload({ legacyDir, config }, { filesystem: fs = nodeFilesystem } = {}) {
|
|
75
|
+
if (!isExistingDir(legacyDir, fs))
|
|
76
76
|
return false;
|
|
77
|
-
return isExistingFile(path.join(legacyDir, config.proofEntry));
|
|
77
|
+
return isExistingFile(path.join(legacyDir, config.proofEntry), fs);
|
|
78
78
|
}
|
|
79
79
|
/**
|
|
80
80
|
* Returns the list of configured payload entries that already exist under
|
|
@@ -85,8 +85,8 @@ export function legacyDirHasPayload(legacyDir, config) {
|
|
|
85
85
|
* @param dataDir - The data root to check.
|
|
86
86
|
* @param config - The host's payload/proof-entry configuration.
|
|
87
87
|
*/
|
|
88
|
-
export function dataDirHasExistingPayload(dataDir, config) {
|
|
89
|
-
if (!isExistingDir(dataDir))
|
|
88
|
+
export function dataDirHasExistingPayload({ dataDir, config }, { filesystem: fs = nodeFilesystem } = {}) {
|
|
89
|
+
if (!isExistingDir(dataDir, fs))
|
|
90
90
|
return [];
|
|
91
91
|
return config.payloadEntries.filter((entry) => fs.existsSync(path.join(dataDir, entry)));
|
|
92
92
|
}
|
|
@@ -95,26 +95,26 @@ export function dataDirHasExistingPayload(dataDir, config) {
|
|
|
95
95
|
* preserved symlink would let a downstream reader escape the data root.
|
|
96
96
|
* @internal
|
|
97
97
|
*/
|
|
98
|
-
function assertNoSymlinks(srcRoot, displayPath = srcRoot) {
|
|
98
|
+
function assertNoSymlinks(srcRoot, fs, displayPath = srcRoot) {
|
|
99
99
|
const stat = fs.lstatSync(srcRoot);
|
|
100
100
|
if (stat.isSymbolicLink()) {
|
|
101
|
-
throw new LegacyMigrationError('symlink_in_payload', `legacy payload contains a symlink at "${displayPath}"; refusing to migrate`);
|
|
101
|
+
throw new LegacyMigrationError({ code: 'symlink_in_payload', message: `legacy payload contains a symlink at "${displayPath}"; refusing to migrate` });
|
|
102
102
|
}
|
|
103
103
|
if (!stat.isDirectory())
|
|
104
104
|
return;
|
|
105
105
|
for (const child of fs.readdirSync(srcRoot)) {
|
|
106
|
-
assertNoSymlinks(path.join(srcRoot, child), path.join(displayPath, child));
|
|
106
|
+
assertNoSymlinks(path.join(srcRoot, child), fs, path.join(displayPath, child));
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
109
|
/** Stage every present payload entry into `stagingDir`. Returns the entries actually copied. @internal */
|
|
110
|
-
function stagePayload(legacyDir, stagingDir, payloadEntries) {
|
|
110
|
+
function stagePayload(legacyDir, stagingDir, payloadEntries, fs) {
|
|
111
111
|
fs.mkdirSync(stagingDir, { recursive: true });
|
|
112
112
|
const copied = [];
|
|
113
113
|
for (const entry of payloadEntries) {
|
|
114
114
|
const src = path.join(legacyDir, entry);
|
|
115
115
|
if (!fs.existsSync(src))
|
|
116
116
|
continue;
|
|
117
|
-
assertNoSymlinks(src);
|
|
117
|
+
assertNoSymlinks(src, fs);
|
|
118
118
|
const dst = path.join(stagingDir, entry);
|
|
119
119
|
fs.cpSync(src, dst, { recursive: true, force: true, errorOnExist: false });
|
|
120
120
|
copied.push(entry);
|
|
@@ -133,7 +133,7 @@ function stagePayload(legacyDir, stagingDir, payloadEntries) {
|
|
|
133
133
|
* @param entries - The payload entries to promote.
|
|
134
134
|
* @returns The entries that were placed into `dataDir` before any failure.
|
|
135
135
|
*/
|
|
136
|
-
export function promoteStaged(stagingDir, dataDir, entries) {
|
|
136
|
+
export function promoteStaged({ stagingDir, dataDir, entries }, { filesystem: fs = nodeFilesystem } = {}) {
|
|
137
137
|
fs.mkdirSync(dataDir, { recursive: true });
|
|
138
138
|
const promoted = [];
|
|
139
139
|
try {
|
|
@@ -157,17 +157,17 @@ export function promoteStaged(stagingDir, dataDir, entries) {
|
|
|
157
157
|
}
|
|
158
158
|
}
|
|
159
159
|
catch (err) {
|
|
160
|
-
rollbackPromoted(dataDir, promoted);
|
|
160
|
+
rollbackPromoted(dataDir, promoted, fs);
|
|
161
161
|
throw err;
|
|
162
162
|
}
|
|
163
163
|
return promoted;
|
|
164
164
|
}
|
|
165
|
-
function rollbackPromoted(dataDir, promoted) {
|
|
165
|
+
function rollbackPromoted(dataDir, promoted, fs) {
|
|
166
166
|
for (const entry of promoted) {
|
|
167
167
|
fs.rmSync(path.join(dataDir, entry), { recursive: true, force: true });
|
|
168
168
|
}
|
|
169
169
|
}
|
|
170
|
-
function writeMarkerDefault(dataDir, legacyDir, markerFile) {
|
|
170
|
+
function writeMarkerDefault({ dataDir, legacyDir, markerFile }, fs) {
|
|
171
171
|
const marker = path.join(dataDir, markerFile);
|
|
172
172
|
const temp = `${marker}.tmp-${process.pid}-${Date.now()}`;
|
|
173
173
|
const payload = JSON.stringify({ legacyDir: path.resolve(legacyDir), migratedAt: new Date().toISOString() }, null, 2);
|
|
@@ -190,8 +190,10 @@ function writeMarkerDefault(dataDir, legacyDir, markerFile) {
|
|
|
190
190
|
* @returns The migration outcome (`noop` when unconfigured, `skipped` when
|
|
191
191
|
* already migrated, `migrated` with the copied entry list otherwise).
|
|
192
192
|
*/
|
|
193
|
-
export function migrateLegacyDataDirSync(
|
|
194
|
-
const
|
|
193
|
+
export function migrateLegacyDataDirSync(requiredArgs, optionalArgs = {}) {
|
|
194
|
+
const options = { ...requiredArgs, ...optionalArgs };
|
|
195
|
+
const fs = options.filesystem ?? nodeFilesystem;
|
|
196
|
+
const log = options.logger ?? { info: ({ message }) => console.log(`[jini-migrate] ${message}`) };
|
|
195
197
|
const markerFile = options.markerFile ?? DEFAULT_MARKER_FILE;
|
|
196
198
|
const raw = options.legacyDir;
|
|
197
199
|
if (raw === undefined || raw.length === 0) {
|
|
@@ -209,31 +211,34 @@ export function migrateLegacyDataDirSync(options) {
|
|
|
209
211
|
if (fs.existsSync(markerPath)) {
|
|
210
212
|
return { status: 'skipped', reason: 'migration marker already present' };
|
|
211
213
|
}
|
|
212
|
-
if (!legacyDirHasPayload(legacyDir, options)) {
|
|
213
|
-
throw new LegacyMigrationError('legacy_dir_invalid', `legacy data dir "${legacyDir}" is not a usable legacy data dir (expected "${options.proofEntry}" directly inside it).`);
|
|
214
|
+
if (!legacyDirHasPayload({ legacyDir: legacyDir, config: options }, { filesystem: fs })) {
|
|
215
|
+
throw new LegacyMigrationError({ code: 'legacy_dir_invalid', message: `legacy data dir "${legacyDir}" is not a usable legacy data dir (expected "${options.proofEntry}" directly inside it).` });
|
|
214
216
|
}
|
|
215
|
-
const existing = dataDirHasExistingPayload(dataDir, options);
|
|
217
|
+
const existing = dataDirHasExistingPayload({ dataDir: dataDir, config: options }, { filesystem: fs });
|
|
216
218
|
if (existing.length > 0) {
|
|
217
|
-
throw new LegacyMigrationError('data_dir_not_empty', `data dir "${dataDir}" already contains payload entries (${existing.join(', ')}); refusing to merge legacy data on top. Move the existing data aside or pick a fresh data root before retrying.`);
|
|
219
|
+
throw new LegacyMigrationError({ code: 'data_dir_not_empty', message: `data dir "${dataDir}" already contains payload entries (${existing.join(', ')}); refusing to merge legacy data on top. Move the existing data aside or pick a fresh data root before retrying.` });
|
|
218
220
|
}
|
|
219
|
-
log.info(`migrating legacy data from "${legacyDir}" to "${dataDir}"`);
|
|
221
|
+
log.info({ message: `migrating legacy data from "${legacyDir}" to "${dataDir}"` });
|
|
220
222
|
fs.mkdirSync(path.dirname(dataDir), { recursive: true });
|
|
221
223
|
const stagingDir = path.join(path.dirname(dataDir), `${path.basename(dataDir)}.migrate-${process.pid}-${Date.now()}`);
|
|
222
224
|
let copied;
|
|
223
225
|
let promoted = [];
|
|
224
226
|
try {
|
|
225
|
-
copied = stagePayload(legacyDir, stagingDir, options.payloadEntries);
|
|
226
|
-
promoted = promoteStaged(stagingDir, dataDir, copied);
|
|
227
|
-
(options.writeMarker
|
|
227
|
+
copied = stagePayload(legacyDir, stagingDir, options.payloadEntries, fs);
|
|
228
|
+
promoted = promoteStaged({ stagingDir: stagingDir, dataDir: dataDir, entries: copied }, { filesystem: fs });
|
|
229
|
+
if (options.writeMarker)
|
|
230
|
+
options.writeMarker({ dataDir, legacyDir, markerFile });
|
|
231
|
+
else
|
|
232
|
+
writeMarkerDefault({ dataDir, legacyDir, markerFile }, fs);
|
|
228
233
|
}
|
|
229
234
|
catch (err) {
|
|
230
|
-
rollbackPromoted(dataDir, promoted);
|
|
235
|
+
rollbackPromoted(dataDir, promoted, fs);
|
|
231
236
|
fs.rmSync(markerPath, { force: true });
|
|
232
237
|
fs.rmSync(stagingDir, { recursive: true, force: true });
|
|
233
238
|
throw err;
|
|
234
239
|
}
|
|
235
240
|
fs.rmSync(stagingDir, { recursive: true, force: true });
|
|
236
|
-
log.info(`migration complete: copied ${copied.length} entr${copied.length === 1 ? 'y' : 'ies'} (${copied.join(', ')})`);
|
|
241
|
+
log.info({ message: `migration complete: copied ${copied.length} entr${copied.length === 1 ? 'y' : 'ies'} (${copied.join(', ')})` });
|
|
237
242
|
return { status: 'migrated', reason: 'copied legacy payload', copied };
|
|
238
243
|
}
|
|
239
244
|
//# sourceMappingURL=legacy-data-migration.js.map
|
|
@@ -0,0 +1,130 @@
|
|
|
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;
|
|
130
|
+
//# sourceMappingURL=read-only-tools.d.ts.map
|
|
@@ -0,0 +1,115 @@
|
|
|
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
|
+
}
|
|
115
|
+
//# sourceMappingURL=read-only-tools.js.map
|
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
* This exists because `DelegatedToolBridge.execute()` (this package's `delegated-tool-bridge.ts`)
|
|
8
8
|
* calls `lifecycle.emit()` directly, in-process — there was previously no way for a caller that
|
|
9
9
|
* executed a tool somewhere else to report the outcome back into this run's log. That absence is
|
|
10
|
-
* what forced `ToolExecutor`/`ToolRegistry`/`RunLifecycle` to
|
|
11
|
-
*
|
|
10
|
+
* what forced `ToolExecutor`/`ToolRegistry`/`RunLifecycle` to live in one process per run: only an
|
|
11
|
+
* in-process lifecycle could append those events. This module supplies the same two event
|
|
12
12
|
* shapes `DelegatedToolBridge.execute()` already emits (`tool_use` then `tool_result`), exposed as
|
|
13
13
|
* a small, explicit recording API a *transport* (this package does not itself expose one — see
|
|
14
|
-
* `@jini-ai/http
|
|
14
|
+
* `@jini-ai/daemon/http`'s `remote-run-events.ts`) can wrap with authentication and call remotely.
|
|
15
15
|
*
|
|
16
16
|
* This does not change how tool execution is authorized. A caller of `recordToolUse`/
|
|
17
17
|
* `recordToolResult` has, by definition, already run the tool through its OWN `ToolExecutor` (with
|
|
@@ -34,9 +34,15 @@ export interface RemoteToolResultRecord {
|
|
|
34
34
|
}
|
|
35
35
|
export interface RemoteToolEventRecorder {
|
|
36
36
|
/** Records that a tool call started. Throws (matching `RunLifecycle.emit`) if `runId` is unknown or already terminal. */
|
|
37
|
-
recordToolUse(
|
|
37
|
+
recordToolUse(args: {
|
|
38
|
+
readonly runId: string;
|
|
39
|
+
readonly record: RemoteToolUseRecord;
|
|
40
|
+
}): Promise<RunProtocolEvent>;
|
|
38
41
|
/** Records that a tool call finished (or failed) — mirrors `DelegatedToolBridge.execute()`'s own `tool_result` shape exactly. */
|
|
39
|
-
recordToolResult(
|
|
42
|
+
recordToolResult(args: {
|
|
43
|
+
readonly runId: string;
|
|
44
|
+
readonly record: RemoteToolResultRecord;
|
|
45
|
+
}): Promise<RunProtocolEvent>;
|
|
40
46
|
}
|
|
41
47
|
export interface CreateRemoteToolEventRecorderOptions {
|
|
42
48
|
readonly lifecycle: RunLifecycle;
|
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
export function createRemoteToolEventRecorder(options) {
|
|
2
2
|
const { lifecycle } = options;
|
|
3
3
|
return {
|
|
4
|
-
recordToolUse(runId, record) {
|
|
5
|
-
return lifecycle.emit(runId, {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
recordToolUse({ runId, record }) {
|
|
5
|
+
return lifecycle.emit({ runId: runId, input: {
|
|
6
|
+
event: 'agent',
|
|
7
|
+
data: { type: 'tool_use', id: record.toolUseId, name: record.toolId, input: record.input },
|
|
8
|
+
} });
|
|
9
9
|
},
|
|
10
|
-
recordToolResult(runId, record) {
|
|
11
|
-
return lifecycle.emit(runId, {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
10
|
+
recordToolResult({ runId, record }) {
|
|
11
|
+
return lifecycle.emit({ runId: runId, input: {
|
|
12
|
+
event: 'agent',
|
|
13
|
+
data: {
|
|
14
|
+
type: 'tool_result',
|
|
15
|
+
toolUseId: record.toolUseId,
|
|
16
|
+
content: record.content,
|
|
17
|
+
...(record.isError ? { isError: true } : {}),
|
|
18
|
+
},
|
|
19
|
+
} });
|
|
20
20
|
},
|
|
21
21
|
};
|
|
22
22
|
}
|
package/dist/routines/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* The routine scheduler ({@link ./scheduler.js}'s `RoutineService`, `./schedule.js`'s DST-safe
|
|
5
5
|
* wall-clock math) plus its HTTP-facing CRUD + run-history counterpart ({@link ./routine-store.js}'s
|
|
6
|
-
* `RoutineStore`). See `../../
|
|
6
|
+
* `RoutineStore`). See `../../archived provenance ledger`'s dated routines section for full provenance.
|
|
7
7
|
*/
|
|
8
8
|
export * from './types.js';
|
|
9
9
|
export * from './schedule.js';
|
package/dist/routines/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* The routine scheduler ({@link ./scheduler.js}'s `RoutineService`, `./schedule.js`'s DST-safe
|
|
5
5
|
* wall-clock math) plus its HTTP-facing CRUD + run-history counterpart ({@link ./routine-store.js}'s
|
|
6
|
-
* `RoutineStore`). See `../../
|
|
6
|
+
* `RoutineStore`). See `../../archived provenance ledger`'s dated routines section for full provenance.
|
|
7
7
|
*/
|
|
8
8
|
export * from './types.js';
|
|
9
9
|
export * from './schedule.js';
|