1111 * one exact package version and integrity value, and verifies that the
1212 * candidate contract artifact name stays outside the all-agent cohort download
1313 * pattern.
14+ *
15+ * It also binds the accepted Pi trust boundary: the baseline permits only the
16+ * managed inference route from root-owned image binaries, writable paths stay
17+ * on the declared sandbox state, non-interactive runs keep passing the flag
18+ * that ignores project-local resources, and neither the project-trust store nor
19+ * the project-trust setting can travel through backup and restore.
1420 */
1521
1622import { createHash } from "node:crypto" ;
@@ -24,6 +30,23 @@ const MANAGED_IMAGE_CONTRACT_PATH = "src/lib/onboard/managed-image/contract.ts";
2430const PI_PACKAGE = "@earendil-works/pi-coding-agent" ;
2531const COHORT_CONTRACT_ARTIFACT_PREFIX = "managed-pr-contract-" ;
2632const CANDIDATE_CONTRACT_ARTIFACT_PREFIX = "managed-candidate-contract-" ;
33+ const PI_MANIFEST_PATH = "agents/pi/manifest.yaml" ;
34+ const PI_POLICY_PATH = "agents/pi/policy-additions.yaml" ;
35+
36+ const MANAGED_INFERENCE_POLICY = "managed_inference" ;
37+ const MANAGED_INFERENCE_HOST = "inference.local" ;
38+ const MANAGED_INFERENCE_PORT = 443 ;
39+ const APPROVED_NETWORK_BINARIES = [
40+ "/usr/local/bin/node" ,
41+ "/usr/local/bin/pi" ,
42+ "/usr/local/lib/nemoclaw/pi-runtime/**" ,
43+ ] ;
44+ const APPROVED_READ_WRITE_PATHS = [ "/dev/null" , "/sandbox" , "/sandbox/.pi" , "/tmp" ] ;
45+ const REQUIRED_LANDLOCK_COMPATIBILITY = "strict" ;
46+ const REQUIRED_SANDBOX_IDENTITY = "sandbox" ;
47+ const NON_INTERACTIVE_APPROVAL_FLAG = "--no-approve" ;
48+ const PROJECT_TRUST_STORE = "trust.json" ;
49+ const PROJECT_TRUST_SETTING = "defaultProjectTrust" ;
2750
2851const REQUIRED_ARTIFACTS = [
2952 "agents/pi/Dockerfile" ,
@@ -46,6 +69,7 @@ export type PiArtifactSources = Readonly<{
4669 managedImagesWorkflow : string ;
4770 manifest : string ;
4871 packageJson : string ;
72+ policyAdditions : string ;
4973} > ;
5074
5175function readDockerfileArg ( source : string , name : string ) : string | null {
@@ -228,12 +252,156 @@ function verifyCohortSeparation(workflow: string): string[] {
228252 return failures ;
229253}
230254
255+ type LooseRecord = Record < string , unknown > ;
256+
257+ function asRecord ( value : unknown ) : LooseRecord {
258+ return typeof value === "object" && value !== null && ! Array . isArray ( value )
259+ ? ( value as LooseRecord )
260+ : { } ;
261+ }
262+
263+ function sortedStrings ( value : unknown ) : string [ ] {
264+ if ( ! Array . isArray ( value ) ) return [ ] ;
265+ return value . filter ( ( entry ) : entry is string => typeof entry === "string" ) . sort ( ) ;
266+ }
267+
268+ function sameSet ( actual : readonly string [ ] , approved : readonly string [ ] ) : boolean {
269+ return actual . length === approved . length && actual . every ( ( entry , index ) => entry === approved [ index ] ) ;
270+ }
271+
272+ function verifyNetworkBoundary ( policy : LooseRecord ) : string [ ] {
273+ const failures : string [ ] = [ ] ;
274+ const networkPolicies = asRecord ( policy . network_policies ) ;
275+ const declared = Object . keys ( networkPolicies ) . sort ( ) ;
276+ if ( ! sameSet ( declared , [ MANAGED_INFERENCE_POLICY ] ) ) {
277+ failures . push (
278+ `${ PI_POLICY_PATH } : the baseline must declare only ${ MANAGED_INFERENCE_POLICY } , found ${ declared . join ( ", " ) || "none" } ` ,
279+ ) ;
280+ return failures ;
281+ }
282+ const managed = asRecord ( networkPolicies [ MANAGED_INFERENCE_POLICY ] ) ;
283+ const endpoints = Array . isArray ( managed . endpoints ) ? managed . endpoints : [ ] ;
284+ if ( endpoints . length !== 1 ) {
285+ failures . push ( `${ PI_POLICY_PATH } : ${ MANAGED_INFERENCE_POLICY } must declare exactly one endpoint` ) ;
286+ }
287+ for ( const entry of endpoints ) {
288+ const endpoint = asRecord ( entry ) ;
289+ if ( endpoint . host !== MANAGED_INFERENCE_HOST || endpoint . port !== MANAGED_INFERENCE_PORT ) {
290+ failures . push (
291+ `${ PI_POLICY_PATH } : the baseline permits only ${ MANAGED_INFERENCE_HOST } :${ String ( MANAGED_INFERENCE_PORT ) } ` ,
292+ ) ;
293+ }
294+ if ( endpoint . enforcement !== "enforce" ) {
295+ failures . push ( `${ PI_POLICY_PATH } : ${ MANAGED_INFERENCE_HOST } must stay enforced, not observed` ) ;
296+ }
297+ const rules = Array . isArray ( endpoint . rules ) ? endpoint . rules : [ ] ;
298+ for ( const rule of rules ) {
299+ const allow = asRecord ( asRecord ( rule ) . allow ) ;
300+ const rulePath = typeof allow . path === "string" ? allow . path : "" ;
301+ if ( ! Object . hasOwn ( asRecord ( rule ) , "allow" ) || ! rulePath . startsWith ( "/v1/" ) ) {
302+ failures . push (
303+ `${ PI_POLICY_PATH } : every managed inference rule must allow an explicit /v1/ route, found ${ rulePath || "an unreadable rule" } ` ,
304+ ) ;
305+ }
306+ }
307+ }
308+ const binaries = sortedStrings (
309+ Array . isArray ( managed . binaries )
310+ ? managed . binaries . map ( ( entry ) => asRecord ( entry ) . path )
311+ : [ ] ,
312+ ) ;
313+ if ( ! sameSet ( binaries , APPROVED_NETWORK_BINARIES ) ) {
314+ failures . push (
315+ `${ PI_POLICY_PATH } : network capability must stay on the root-owned image binaries ${ APPROVED_NETWORK_BINARIES . join ( ", " ) } ` ,
316+ ) ;
317+ }
318+ return failures ;
319+ }
320+
321+ function verifyFilesystemBoundary ( policy : LooseRecord ) : string [ ] {
322+ const failures : string [ ] = [ ] ;
323+ const filesystem = asRecord ( policy . filesystem_policy ) ;
324+ const readWrite = sortedStrings ( filesystem . read_write ) ;
325+ if ( ! sameSet ( readWrite , APPROVED_READ_WRITE_PATHS ) ) {
326+ failures . push (
327+ `${ PI_POLICY_PATH } : writable paths must stay ${ APPROVED_READ_WRITE_PATHS . join ( ", " ) } , found ${ readWrite . join ( ", " ) || "none" } ` ,
328+ ) ;
329+ }
330+ if ( asRecord ( policy . landlock ) . compatibility !== REQUIRED_LANDLOCK_COMPATIBILITY ) {
331+ failures . push (
332+ `${ PI_POLICY_PATH } : landlock.compatibility must be ${ REQUIRED_LANDLOCK_COMPATIBILITY } so filesystem policy fails closed` ,
333+ ) ;
334+ }
335+ const process = asRecord ( policy . process ) ;
336+ if (
337+ process . run_as_user !== REQUIRED_SANDBOX_IDENTITY ||
338+ process . run_as_group !== REQUIRED_SANDBOX_IDENTITY
339+ ) {
340+ failures . push ( `${ PI_POLICY_PATH } : Pi must run as the ${ REQUIRED_SANDBOX_IDENTITY } user and group` ) ;
341+ }
342+ return failures ;
343+ }
344+
345+ function verifyApprovalBoundary ( manifest : LooseRecord ) : string [ ] {
346+ const failures : string [ ] = [ ] ;
347+ const runtime = asRecord ( manifest . runtime ) ;
348+ const headless = typeof runtime . headless_command === "string" ? runtime . headless_command : "" ;
349+ if ( ! headless . split ( / \s + / u) . includes ( NON_INTERACTIVE_APPROVAL_FLAG ) ) {
350+ failures . push (
351+ `${ PI_MANIFEST_PATH } : runtime.headless_command must pass ${ NON_INTERACTIVE_APPROVAL_FLAG } so non-interactive runs ignore project-local resources` ,
352+ ) ;
353+ }
354+ if ( asRecord ( manifest . mcp ) . support !== "disabled" ) {
355+ failures . push ( `${ PI_MANIFEST_PATH } : mcp.support must stay disabled for the accepted v1 surface` ) ;
356+ }
357+ if ( manifest . device_pairing !== false ) {
358+ failures . push ( `${ PI_MANIFEST_PATH } : device_pairing must stay false for the accepted v1 surface` ) ;
359+ }
360+ return failures ;
361+ }
362+
363+ function verifyProjectTrustBoundary ( manifest : LooseRecord ) : string [ ] {
364+ const failures : string [ ] = [ ] ;
365+ const stateDirs = Array . isArray ( manifest . state_dirs ) ? manifest . state_dirs : [ ] ;
366+ const stateFiles = Array . isArray ( manifest . state_files ) ? manifest . state_files : [ ] ;
367+ const declared = [ ...stateDirs , ...stateFiles ] . map ( ( entry ) => asRecord ( entry ) . path ) ;
368+ if ( declared . includes ( PROJECT_TRUST_STORE ) ) {
369+ failures . push (
370+ `${ PI_MANIFEST_PATH } : ${ PROJECT_TRUST_STORE } must stay undeclared so a restore cannot carry a project-trust decision` ,
371+ ) ;
372+ }
373+ for ( const entry of stateFiles ) {
374+ const stateFile = asRecord ( entry ) ;
375+ const userKeys = Array . isArray ( asRecord ( stateFile . restore ) . user_keys )
376+ ? ( asRecord ( stateFile . restore ) . user_keys as unknown [ ] )
377+ : [ ] ;
378+ if ( userKeys . map ( ( key ) => asRecord ( key ) . key ) . includes ( PROJECT_TRUST_SETTING ) ) {
379+ failures . push (
380+ `${ PI_MANIFEST_PATH } : ${ PROJECT_TRUST_SETTING } must stay outside the restore allowlist so a backup cannot widen project trust` ,
381+ ) ;
382+ }
383+ }
384+ return failures ;
385+ }
386+
387+ export function verifyPiTrustBoundary ( sources : PiArtifactSources ) : string [ ] {
388+ const policy = asRecord ( parseYaml ( sources . policyAdditions ) ) ;
389+ const manifest = asRecord ( parseYaml ( sources . manifest ) ) ;
390+ return [
391+ ...verifyNetworkBoundary ( policy ) ,
392+ ...verifyFilesystemBoundary ( policy ) ,
393+ ...verifyApprovalBoundary ( manifest ) ,
394+ ...verifyProjectTrustBoundary ( manifest ) ,
395+ ] ;
396+ }
397+
231398export function verifyPiCandidateArtifacts ( sources : PiArtifactSources ) : string [ ] {
232399 return [
233400 ...verifyPinnedIdentity ( sources ) ,
234401 ...verifyCandidateRegistration ( sources . managedImageContract ) ,
235402 ...verifyCohortSeparation ( sources . managedImagesWorkflow ) ,
236403 ...verifyManagedImageDeclaration ( sources ) ,
404+ ...verifyPiTrustBoundary ( sources ) ,
237405 ] ;
238406}
239407
@@ -256,8 +424,9 @@ function main(): void {
256424 lock : readRepoFile ( "agents/pi/pi-runtime/package-lock.json" ) ,
257425 managedImageContract : readRepoFile ( MANAGED_IMAGE_CONTRACT_PATH ) ,
258426 managedImagesWorkflow : readRepoFile ( ".github/workflows/managed-images.yaml" ) ,
259- manifest : readRepoFile ( "agents/pi/manifest.yaml" ) ,
427+ manifest : readRepoFile ( PI_MANIFEST_PATH ) ,
260428 packageJson : readRepoFile ( "agents/pi/pi-runtime/package.json" ) ,
429+ policyAdditions : readRepoFile ( PI_POLICY_PATH ) ,
261430 } ) ;
262431 if ( failures . length > 0 ) {
263432 console . error ( failures . join ( "\n" ) ) ;
0 commit comments