pi-daddy 0.44.3 → 0.45.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 +11 -0
- package/README.md +48 -0
- package/dist/kernel/definition-sources.d.ts +13 -0
- package/dist/kernel/definition-sources.d.ts.map +1 -0
- package/dist/kernel/definition-sources.js +2 -0
- package/dist/kernel/definition-sources.js.map +1 -0
- package/dist/kernel/definitions.d.ts +2 -0
- package/dist/kernel/definitions.d.ts.map +1 -1
- package/dist/kernel/definitions.js.map +1 -1
- package/dist/kernel/env-names.d.ts +2 -0
- package/dist/kernel/env-names.d.ts.map +1 -1
- package/dist/kernel/env-names.js +3 -0
- package/dist/kernel/env-names.js.map +1 -1
- package/dist/kernel/propagation.d.ts +1 -1
- package/dist/kernel/propagation.d.ts.map +1 -1
- package/dist/kernel/propagation.js +2 -0
- package/dist/kernel/propagation.js.map +1 -1
- package/dist/kernel/selected-definitions.d.ts +1 -1
- package/dist/kernel/selected-definitions.d.ts.map +1 -1
- package/dist/kernel/selected-definitions.js +19 -10
- package/dist/kernel/selected-definitions.js.map +1 -1
- package/dist/products/public-evidence.d.ts +62 -0
- package/dist/products/public-evidence.d.ts.map +1 -0
- package/dist/products/public-evidence.js +116 -0
- package/dist/products/public-evidence.js.map +1 -0
- package/extensions/definition-describe.ts +4 -1
- package/extensions/delegate-chain.ts +22 -12
- package/extensions/delegation.ts +58 -26
- package/extensions/execution-evidence.ts +103 -0
- package/extensions/public-evidence.ts +90 -0
- package/extensions/run-delegation.ts +20 -4
- package/extensions/session.ts +10 -1
- package/package.json +1 -1
- package/src/kernel/definition-sources.ts +12 -0
- package/src/kernel/definitions.ts +2 -0
- package/src/kernel/env-names.ts +3 -0
- package/src/kernel/propagation.ts +2 -0
- package/src/kernel/selected-definitions.ts +31 -10
- package/src/products/public-evidence.ts +181 -0
|
@@ -5,6 +5,7 @@ import { lstat, realpath } from "node:fs/promises";
|
|
|
5
5
|
import { BoundedReadCleanupError, readBoundedBytes } from "./bounded-read.ts";
|
|
6
6
|
import { ceilingForDefinition, parseSkillDefinition, type SkillDefinition } from "./definitions.ts";
|
|
7
7
|
import { resolve } from "./resolve.ts";
|
|
8
|
+
import type { DefinitionSource } from "./definition-sources.ts";
|
|
8
9
|
import type { CatalogEntry } from "./catalog.ts";
|
|
9
10
|
export interface SelectedCommand {
|
|
10
11
|
source?: string;
|
|
@@ -16,13 +17,20 @@ const hash = (value: string) => createHash("sha256").update(value, "utf8").diges
|
|
|
16
17
|
const object = (x: unknown): x is Record<string, unknown> => !!x && typeof x === "object" && !Array.isArray(x);
|
|
17
18
|
const exact = (x: Record<string, unknown>, keys: string[]) =>
|
|
18
19
|
Object.keys(x).sort().join(",") === [...keys].sort().join(",");
|
|
19
|
-
async function read(path: string): Promise<string> {
|
|
20
|
+
async function read(path: string, kind: DefinitionSource["kind"], sources?: DefinitionSource[]): Promise<string> {
|
|
20
21
|
if (!(await lstat(path)).isFile()) throw Error(`nonregular selected resource: ${path}`);
|
|
21
22
|
const result = await readBoundedBytes(path, { maxBytes: 1024 * 1024, timeoutMs: 3000 });
|
|
22
23
|
if (!result.ok) throw Error(result.detail);
|
|
23
|
-
|
|
24
|
+
const text = new TextDecoder("utf-8", { fatal: true }).decode(result.bytes);
|
|
25
|
+
sources?.push(Object.freeze({ kind, path, base64: result.bytes.toString("base64") }));
|
|
26
|
+
return text;
|
|
24
27
|
}
|
|
25
|
-
async function principalBinding(
|
|
28
|
+
async function principalBinding(
|
|
29
|
+
path: string,
|
|
30
|
+
inline: SkillDefinition,
|
|
31
|
+
skillText: string,
|
|
32
|
+
sources?: DefinitionSource[],
|
|
33
|
+
): Promise<SkillDefinition> {
|
|
26
34
|
const phase = inline.name;
|
|
27
35
|
const marked = inline.metadata?.["principal-package"] === "principal-pi-skills";
|
|
28
36
|
if (!phases.includes(phase) || path !== join(dirname(dirname(path)), phase, "SKILL.md")) {
|
|
@@ -32,7 +40,7 @@ async function principalBinding(path: string, inline: SkillDefinition, skillText
|
|
|
32
40
|
const root = dirname(dirname(path));
|
|
33
41
|
let packageText: string;
|
|
34
42
|
try {
|
|
35
|
-
packageText = await read(join(root, "package.json"));
|
|
43
|
+
packageText = await read(join(root, "package.json"), "package", sources);
|
|
36
44
|
} catch (error) {
|
|
37
45
|
if ((error as NodeJS.ErrnoException).code === "ENOENT" && !marked) return inline;
|
|
38
46
|
throw error;
|
|
@@ -42,7 +50,7 @@ async function principalBinding(path: string, inline: SkillDefinition, skillText
|
|
|
42
50
|
if (marked) throw Error("marked Principal skill has missing or wrong package identity");
|
|
43
51
|
return inline;
|
|
44
52
|
}
|
|
45
|
-
const manifest: unknown = JSON.parse(await read(join(root, "principal-agents.json")));
|
|
53
|
+
const manifest: unknown = JSON.parse(await read(join(root, "principal-agents.json"), "binding-manifest", sources));
|
|
46
54
|
if (
|
|
47
55
|
!object(manifest) ||
|
|
48
56
|
!exact(manifest, ["version", "package", "bindings"]) ||
|
|
@@ -73,7 +81,7 @@ async function principalBinding(path: string, inline: SkillDefinition, skillText
|
|
|
73
81
|
(await realpath(path)) !== join(await realpath(root), row.skill)
|
|
74
82
|
)
|
|
75
83
|
throw Error("Principal binding path escaped its selected package");
|
|
76
|
-
const agentText = await read(agentPath);
|
|
84
|
+
const agentText = await read(agentPath, "delegated-agent", sources);
|
|
77
85
|
if (hash(skillText) !== row.skillSha256 || hash(agentText) !== row.agentSha256)
|
|
78
86
|
throw Error("Principal binding hash mismatch");
|
|
79
87
|
const agent = parseSkillDefinition(agentPath, agentText);
|
|
@@ -106,7 +114,10 @@ async function principalBinding(path: string, inline: SkillDefinition, skillText
|
|
|
106
114
|
binding: Object.freeze({ package: "principal-pi-skills", phase }),
|
|
107
115
|
};
|
|
108
116
|
}
|
|
109
|
-
export async function selectedDefinitions(
|
|
117
|
+
export async function selectedDefinitions(
|
|
118
|
+
commands: readonly SelectedCommand[],
|
|
119
|
+
captureSources = false,
|
|
120
|
+
): Promise<{
|
|
110
121
|
definitions: Map<string, SkillDefinition>;
|
|
111
122
|
skills: CatalogEntry[];
|
|
112
123
|
skips: string[];
|
|
@@ -127,10 +138,11 @@ export async function selectedDefinitions(commands: readonly SelectedCommand[]):
|
|
|
127
138
|
try {
|
|
128
139
|
if (!command.sourceInfo?.path) throw Error("selected resource lacks a public source path");
|
|
129
140
|
const path = resolvePath(command.sourceInfo.path);
|
|
130
|
-
const
|
|
141
|
+
const sources: DefinitionSource[] | undefined = captureSources ? [] : undefined;
|
|
142
|
+
const text = await read(path, "selected-skill", sources);
|
|
131
143
|
const parsed = parseSkillDefinition(path, text);
|
|
132
144
|
if (!parsed || parsed.name !== name) throw Error("selected name/path does not match definition");
|
|
133
|
-
const bound = await principalBinding(path, parsed, text);
|
|
145
|
+
const bound = await principalBinding(path, parsed, text, sources);
|
|
134
146
|
const definitionId = hash(
|
|
135
147
|
JSON.stringify({
|
|
136
148
|
name,
|
|
@@ -145,7 +157,16 @@ export async function selectedDefinitions(commands: readonly SelectedCommand[]):
|
|
|
145
157
|
}),
|
|
146
158
|
);
|
|
147
159
|
if (bound.metadata) Object.freeze(bound.metadata);
|
|
148
|
-
definitions.set(
|
|
160
|
+
definitions.set(
|
|
161
|
+
name,
|
|
162
|
+
Object.freeze({
|
|
163
|
+
...bound,
|
|
164
|
+
definitionId,
|
|
165
|
+
...(sources
|
|
166
|
+
? { sourceSnapshot: Object.freeze({ resources: Object.freeze(sources), body: bound.body }) }
|
|
167
|
+
: {}),
|
|
168
|
+
}),
|
|
169
|
+
);
|
|
149
170
|
skills.push({ capability: `skill:${name}`, kind: "skill", source: path });
|
|
150
171
|
} catch (error) {
|
|
151
172
|
if (error instanceof BoundedReadCleanupError) throw error;
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/** Exact public extension-return capture. Opt-in local observation, never an authority/acceptance source.
|
|
2
|
+
* Existing operator-created root must be canonical, private and owned by this uid. Held directory
|
|
3
|
+
* descriptors anchor every exclusive write; successful refs are returned only after fsync and close.
|
|
4
|
+
* Hashes detect accidental edits; this does not authenticate against a hostile process with the same uid.
|
|
5
|
+
*/
|
|
6
|
+
import { randomUUID, createHash } from "node:crypto";
|
|
7
|
+
import {
|
|
8
|
+
constants,
|
|
9
|
+
openSync,
|
|
10
|
+
closeSync,
|
|
11
|
+
fstatSync,
|
|
12
|
+
fsyncSync,
|
|
13
|
+
writeFileSync,
|
|
14
|
+
mkdirSync,
|
|
15
|
+
realpathSync,
|
|
16
|
+
statSync,
|
|
17
|
+
} from "node:fs";
|
|
18
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
19
|
+
import type { DefinitionSourceSnapshot } from "../kernel/definition-sources.ts";
|
|
20
|
+
export const PUBLIC_EVIDENCE_SCHEMA = "pi-daddy-public-evidence-v1";
|
|
21
|
+
export const MAX_PUBLIC_EVIDENCE_BYTES = 64 * 1024 * 1024;
|
|
22
|
+
export const MAX_PUBLIC_EVIDENCE_FILES = 64;
|
|
23
|
+
export interface PublicEvidenceRef {
|
|
24
|
+
path: string;
|
|
25
|
+
sha256: string;
|
|
26
|
+
bytes: number;
|
|
27
|
+
}
|
|
28
|
+
export interface PublicEvidenceOwner {
|
|
29
|
+
readonly directory: string;
|
|
30
|
+
readonly ownerId: string;
|
|
31
|
+
}
|
|
32
|
+
export interface PublicDefinitionEvidence {
|
|
33
|
+
agent: string;
|
|
34
|
+
definitionId: string;
|
|
35
|
+
sourceHash: string | null;
|
|
36
|
+
bodySha256: string;
|
|
37
|
+
binding: Readonly<{ package: string; phase: string }> | null;
|
|
38
|
+
snapshot?: DefinitionSourceSnapshot;
|
|
39
|
+
}
|
|
40
|
+
export interface PublicEvidenceRequest {
|
|
41
|
+
ordinal: number;
|
|
42
|
+
agent: string | null;
|
|
43
|
+
requestedDefinitionId: string | null;
|
|
44
|
+
definitionId: string | null;
|
|
45
|
+
executionId: string | null;
|
|
46
|
+
}
|
|
47
|
+
export interface PublicFinalEvidence {
|
|
48
|
+
ordinal: number;
|
|
49
|
+
executionId: string | null;
|
|
50
|
+
final: {
|
|
51
|
+
state: "complete";
|
|
52
|
+
text: string;
|
|
53
|
+
sessionId: string;
|
|
54
|
+
messageId: string;
|
|
55
|
+
leafId: string;
|
|
56
|
+
sha256: string;
|
|
57
|
+
} | null;
|
|
58
|
+
}
|
|
59
|
+
export interface PublicEvidenceInput {
|
|
60
|
+
toolCallId: string;
|
|
61
|
+
tool: "delegate_describe" | "delegate" | "delegate_all" | "delegate_chain";
|
|
62
|
+
response: { isError: boolean; content: readonly { type: "text"; text: string }[] };
|
|
63
|
+
requested: readonly PublicEvidenceRequest[];
|
|
64
|
+
runtimeEvidence: unknown;
|
|
65
|
+
definitions: readonly PublicDefinitionEvidence[];
|
|
66
|
+
finals: readonly PublicFinalEvidence[];
|
|
67
|
+
}
|
|
68
|
+
export function createPublicEvidenceOwner(directory: string | undefined): PublicEvidenceOwner | undefined {
|
|
69
|
+
return directory === undefined || directory === "off"
|
|
70
|
+
? undefined
|
|
71
|
+
: Object.freeze({ directory, ownerId: randomUUID() });
|
|
72
|
+
}
|
|
73
|
+
const sha256 = (bytes: Uint8Array) => createHash("sha256").update(bytes).digest("hex");
|
|
74
|
+
const json = (value: unknown) => Buffer.from(JSON.stringify(value) + "\n", "utf8");
|
|
75
|
+
/** No schema accepts arbitrary filenames, output paths, raw arguments, environment, or native details. */
|
|
76
|
+
export function capturePublicEvidence(owner: PublicEvidenceOwner, input: PublicEvidenceInput): PublicEvidenceRef {
|
|
77
|
+
const directory = owner.directory;
|
|
78
|
+
if (
|
|
79
|
+
process.platform !== "linux" ||
|
|
80
|
+
!isAbsolute(directory) ||
|
|
81
|
+
resolve(directory) !== directory ||
|
|
82
|
+
realpathSync(directory) !== directory
|
|
83
|
+
)
|
|
84
|
+
throw Error("public evidence root must be an existing canonical absolute Linux directory");
|
|
85
|
+
const rootFd = openSync(
|
|
86
|
+
directory,
|
|
87
|
+
constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW | constants.O_NONBLOCK,
|
|
88
|
+
);
|
|
89
|
+
try {
|
|
90
|
+
const root = fstatSync(rootFd);
|
|
91
|
+
if (!root.isDirectory() || root.uid !== process.getuid?.() || (root.mode & 0o077) !== 0)
|
|
92
|
+
throw Error("public evidence root must be owned by this user and private (mode 0700)");
|
|
93
|
+
const captureId = randomUUID(),
|
|
94
|
+
target = join(directory, captureId);
|
|
95
|
+
const rootPath = `/proc/self/fd/${rootFd}`;
|
|
96
|
+
mkdirSync(join(rootPath, captureId), { mode: 0o700 });
|
|
97
|
+
const captureFd = openSync(
|
|
98
|
+
join(rootPath, captureId),
|
|
99
|
+
constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW,
|
|
100
|
+
);
|
|
101
|
+
let manifest: PublicEvidenceRef;
|
|
102
|
+
try {
|
|
103
|
+
let bytes = 0,
|
|
104
|
+
files = 0;
|
|
105
|
+
const put = (name: string, content: Buffer): PublicEvidenceRef => {
|
|
106
|
+
bytes += content.byteLength;
|
|
107
|
+
files += 1;
|
|
108
|
+
if (bytes > MAX_PUBLIC_EVIDENCE_BYTES || files > MAX_PUBLIC_EVIDENCE_FILES)
|
|
109
|
+
throw Error("public evidence capture exceeds its byte/file limit; no complete manifest published");
|
|
110
|
+
const fd = openSync(
|
|
111
|
+
join(`/proc/self/fd/${captureFd}`, name),
|
|
112
|
+
constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW | constants.O_NONBLOCK,
|
|
113
|
+
0o600,
|
|
114
|
+
);
|
|
115
|
+
try {
|
|
116
|
+
if (!fstatSync(fd).isFile()) throw Error("public evidence target is not an ordinary file");
|
|
117
|
+
writeFileSync(fd, content);
|
|
118
|
+
fsyncSync(fd);
|
|
119
|
+
} finally {
|
|
120
|
+
closeSync(fd);
|
|
121
|
+
}
|
|
122
|
+
return { path: join(target, name), sha256: sha256(content), bytes: content.byteLength };
|
|
123
|
+
};
|
|
124
|
+
const response = put("response.json", json(input.response));
|
|
125
|
+
const finals = input.finals.map((outcome) => {
|
|
126
|
+
if (!outcome.final) return { ordinal: outcome.ordinal, executionId: outcome.executionId, final: null };
|
|
127
|
+
const { text, ...identity } = outcome.final;
|
|
128
|
+
const bytes = Buffer.from(text, "utf8");
|
|
129
|
+
if (sha256(bytes) !== identity.sha256) throw Error("attributed final bytes do not match the native final hash");
|
|
130
|
+
if (!text || !input.response.content.some((block) => block.text.includes(text)))
|
|
131
|
+
throw Error("attributed final is not present in the public returned content");
|
|
132
|
+
return {
|
|
133
|
+
ordinal: outcome.ordinal,
|
|
134
|
+
executionId: outcome.executionId,
|
|
135
|
+
final: { ...identity, content: put(`outcome-${outcome.ordinal}-final.txt`, bytes) },
|
|
136
|
+
};
|
|
137
|
+
});
|
|
138
|
+
const definitions = input.definitions.map((definition, index) => {
|
|
139
|
+
if (!definition.snapshot) throw Error(`selected source snapshot unavailable for ${definition.agent}`);
|
|
140
|
+
const resources = definition.snapshot.resources.map((source, resource) => ({
|
|
141
|
+
kind: source.kind,
|
|
142
|
+
path: source.path,
|
|
143
|
+
copy: put(`definition-${index}-source-${resource}.bin`, Buffer.from(source.base64, "base64")),
|
|
144
|
+
}));
|
|
145
|
+
const { snapshot: _snapshot, ...identity } = definition;
|
|
146
|
+
return {
|
|
147
|
+
...identity,
|
|
148
|
+
resources,
|
|
149
|
+
body: put(`definition-${index}-body.txt`, Buffer.from(definition.snapshot.body, "utf8")),
|
|
150
|
+
};
|
|
151
|
+
});
|
|
152
|
+
manifest = put(
|
|
153
|
+
"manifest.json",
|
|
154
|
+
json({
|
|
155
|
+
schema: PUBLIC_EVIDENCE_SCHEMA,
|
|
156
|
+
version: 1,
|
|
157
|
+
captureId,
|
|
158
|
+
ownerId: owner.ownerId,
|
|
159
|
+
toolCallId: input.toolCallId,
|
|
160
|
+
tool: input.tool,
|
|
161
|
+
state: "returned",
|
|
162
|
+
response,
|
|
163
|
+
requested: input.requested,
|
|
164
|
+
runtimeEvidence: input.runtimeEvidence,
|
|
165
|
+
finals,
|
|
166
|
+
definitions,
|
|
167
|
+
}),
|
|
168
|
+
);
|
|
169
|
+
fsyncSync(captureFd);
|
|
170
|
+
} finally {
|
|
171
|
+
closeSync(captureFd);
|
|
172
|
+
}
|
|
173
|
+
fsyncSync(rootFd);
|
|
174
|
+
const current = statSync(directory);
|
|
175
|
+
if (realpathSync(directory) !== directory || current.dev !== root.dev || current.ino !== root.ino)
|
|
176
|
+
throw Error("public evidence root changed before reference publication");
|
|
177
|
+
return manifest;
|
|
178
|
+
} finally {
|
|
179
|
+
closeSync(rootFd);
|
|
180
|
+
}
|
|
181
|
+
}
|