@peakflo/agent-harness 0.2.12 → 0.2.13-rc.1
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/dist/bootstrap-handoff.d.ts +73 -0
- package/dist/bootstrap-handoff.js +153 -0
- package/dist/cli.js +23 -8
- package/dist/credential-store.d.ts +3 -2
- package/dist/credential-store.js +7 -7
- package/dist/options.d.ts +5 -4
- package/dist/options.js +5 -4
- package/dist/types.d.ts +9 -5
- package/dist/types.js +9 -5
- package/package.json +4 -1
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PRIVATE CREDENTIAL HANDOFF FROM pf-bootstrap — never through the environment.
|
|
3
|
+
*
|
|
4
|
+
* THE PROBLEM. pf-bootstrap used to hand the enrolment token (cold boot) and
|
|
5
|
+
* the LIVE runner credential (warm in-place upgrade) to the harness in
|
|
6
|
+
* PF_ENROLMENT_TOKEN / PF_RUNNER_CREDENTIAL. Deleting them from `process.env`
|
|
7
|
+
* afterwards (see `scrubCredentialEnv`) keeps them out of child environments,
|
|
8
|
+
* but `/proc/<pid>/environ` is the execve SNAPSHOT and keeps them for the
|
|
9
|
+
* life of the process. Pi and every agent subprocess run as the same uid as
|
|
10
|
+
* the harness, and the kernel lets a same-uid process read another's environ.
|
|
11
|
+
* Confirmed on staging: an agent could read the live runner credential.
|
|
12
|
+
*
|
|
13
|
+
* THE FIX. pf-bootstrap already spawns the harness with a private IPC channel
|
|
14
|
+
* (`stdio: [..., 'ipc']`, a socketpair only the two processes hold). It now
|
|
15
|
+
* sets the NON-secret marker `PF_CREDENTIAL_HANDOFF=ipc` and puts no secret in
|
|
16
|
+
* the environment at all. At startup the harness sends ONE request on that
|
|
17
|
+
* channel and pf-bootstrap answers ONCE with the credentials. They live only
|
|
18
|
+
* in this process's heap from then on.
|
|
19
|
+
*
|
|
20
|
+
* Request/response rather than "bootstrap pushes on spawn": a message that
|
|
21
|
+
* arrives before a listener exists is dropped, and asking first also means
|
|
22
|
+
* pf-bootstrap never writes a secret to a channel nobody is reading.
|
|
23
|
+
*
|
|
24
|
+
* COMPATIBILITY. Without the marker (an older pf-bootstrap image, a
|
|
25
|
+
* self-hosted runner, the desktop) nothing here runs and the environment path
|
|
26
|
+
* works exactly as before, followed by the same scrub. pf-bootstrap only sets
|
|
27
|
+
* the marker for a harness whose package.json declares
|
|
28
|
+
* `peakflo.credentialHandoff: "ipc"` (see package.json), so an older harness
|
|
29
|
+
* installed by a newer image still gets its token the old way.
|
|
30
|
+
*/
|
|
31
|
+
/** The non-secret marker pf-bootstrap sets when it will answer over IPC. */
|
|
32
|
+
export declare const CREDENTIAL_HANDOFF_ENV = "PF_CREDENTIAL_HANDOFF";
|
|
33
|
+
export declare const CREDENTIAL_HANDOFF_IPC = "ipc";
|
|
34
|
+
/** harness -> bootstrap. */
|
|
35
|
+
export declare const CREDENTIAL_REQUEST_TYPE = "pf-harness-credentials-request";
|
|
36
|
+
/** bootstrap -> harness. */
|
|
37
|
+
export declare const CREDENTIAL_RESPONSE_TYPE = "pf-bootstrap-credentials";
|
|
38
|
+
export declare const CREDENTIAL_HANDOFF_TIMEOUT_MS = 15000;
|
|
39
|
+
/** The variables that may carry a credential. Never left in `process.env`. */
|
|
40
|
+
export declare const CREDENTIAL_ENV_KEYS: readonly ["PF_ENROLMENT_TOKEN", "PF_RUNNER_CREDENTIAL", "PF_RUNNER_ID", "PF_RUNNER_TENANT_ID", "PF_RUNNER_CREDENTIAL_EXPIRES_AT"];
|
|
41
|
+
export interface BootstrapCredentials {
|
|
42
|
+
/** Cold boot: the enrolment token. Warm upgrade: the live runner credential. */
|
|
43
|
+
enrolmentToken: string;
|
|
44
|
+
runnerId?: string;
|
|
45
|
+
runnerCredential?: string;
|
|
46
|
+
tenantId?: string | null;
|
|
47
|
+
credentialExpiresAt?: string | null;
|
|
48
|
+
}
|
|
49
|
+
/** The slice of `process` this needs, so tests can pass a fake. */
|
|
50
|
+
export interface HandoffProcess {
|
|
51
|
+
send?: (message: unknown) => boolean;
|
|
52
|
+
on(event: 'message', listener: (message: unknown) => void): unknown;
|
|
53
|
+
off(event: 'message', listener: (message: unknown) => void): unknown;
|
|
54
|
+
}
|
|
55
|
+
export declare class CredentialHandoffError extends Error {
|
|
56
|
+
constructor(message: string);
|
|
57
|
+
}
|
|
58
|
+
/** Remove every credential-bearing variable. Idempotent. */
|
|
59
|
+
export declare function scrubCredentialEnv(env: NodeJS.ProcessEnv): void;
|
|
60
|
+
/**
|
|
61
|
+
* Ask pf-bootstrap for the credentials over the private IPC channel.
|
|
62
|
+
*
|
|
63
|
+
* Returns null — and does nothing — when the marker is absent: the caller then
|
|
64
|
+
* uses the environment exactly as before. The marker itself is removed either
|
|
65
|
+
* way, so agent subprocesses do not inherit it.
|
|
66
|
+
*/
|
|
67
|
+
export declare function receiveBootstrapCredentials(proc: HandoffProcess, env: NodeJS.ProcessEnv, timeoutMs?: number): Promise<BootstrapCredentials | null>;
|
|
68
|
+
/**
|
|
69
|
+
* The credentials as the environment-shaped variables `parseOptions` and
|
|
70
|
+
* `takeHandedOffCredential` already understand. The result is merged into a
|
|
71
|
+
* PLAIN OBJECT copy of the environment, never into `process.env`.
|
|
72
|
+
*/
|
|
73
|
+
export declare function credentialsAsEnv(credentials: BootstrapCredentials): NodeJS.ProcessEnv;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* PRIVATE CREDENTIAL HANDOFF FROM pf-bootstrap — never through the environment.
|
|
4
|
+
*
|
|
5
|
+
* THE PROBLEM. pf-bootstrap used to hand the enrolment token (cold boot) and
|
|
6
|
+
* the LIVE runner credential (warm in-place upgrade) to the harness in
|
|
7
|
+
* PF_ENROLMENT_TOKEN / PF_RUNNER_CREDENTIAL. Deleting them from `process.env`
|
|
8
|
+
* afterwards (see `scrubCredentialEnv`) keeps them out of child environments,
|
|
9
|
+
* but `/proc/<pid>/environ` is the execve SNAPSHOT and keeps them for the
|
|
10
|
+
* life of the process. Pi and every agent subprocess run as the same uid as
|
|
11
|
+
* the harness, and the kernel lets a same-uid process read another's environ.
|
|
12
|
+
* Confirmed on staging: an agent could read the live runner credential.
|
|
13
|
+
*
|
|
14
|
+
* THE FIX. pf-bootstrap already spawns the harness with a private IPC channel
|
|
15
|
+
* (`stdio: [..., 'ipc']`, a socketpair only the two processes hold). It now
|
|
16
|
+
* sets the NON-secret marker `PF_CREDENTIAL_HANDOFF=ipc` and puts no secret in
|
|
17
|
+
* the environment at all. At startup the harness sends ONE request on that
|
|
18
|
+
* channel and pf-bootstrap answers ONCE with the credentials. They live only
|
|
19
|
+
* in this process's heap from then on.
|
|
20
|
+
*
|
|
21
|
+
* Request/response rather than "bootstrap pushes on spawn": a message that
|
|
22
|
+
* arrives before a listener exists is dropped, and asking first also means
|
|
23
|
+
* pf-bootstrap never writes a secret to a channel nobody is reading.
|
|
24
|
+
*
|
|
25
|
+
* COMPATIBILITY. Without the marker (an older pf-bootstrap image, a
|
|
26
|
+
* self-hosted runner, the desktop) nothing here runs and the environment path
|
|
27
|
+
* works exactly as before, followed by the same scrub. pf-bootstrap only sets
|
|
28
|
+
* the marker for a harness whose package.json declares
|
|
29
|
+
* `peakflo.credentialHandoff: "ipc"` (see package.json), so an older harness
|
|
30
|
+
* installed by a newer image still gets its token the old way.
|
|
31
|
+
*/
|
|
32
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
33
|
+
exports.CredentialHandoffError = exports.CREDENTIAL_ENV_KEYS = exports.CREDENTIAL_HANDOFF_TIMEOUT_MS = exports.CREDENTIAL_RESPONSE_TYPE = exports.CREDENTIAL_REQUEST_TYPE = exports.CREDENTIAL_HANDOFF_IPC = exports.CREDENTIAL_HANDOFF_ENV = void 0;
|
|
34
|
+
exports.scrubCredentialEnv = scrubCredentialEnv;
|
|
35
|
+
exports.receiveBootstrapCredentials = receiveBootstrapCredentials;
|
|
36
|
+
exports.credentialsAsEnv = credentialsAsEnv;
|
|
37
|
+
/** The non-secret marker pf-bootstrap sets when it will answer over IPC. */
|
|
38
|
+
exports.CREDENTIAL_HANDOFF_ENV = 'PF_CREDENTIAL_HANDOFF';
|
|
39
|
+
exports.CREDENTIAL_HANDOFF_IPC = 'ipc';
|
|
40
|
+
/** harness -> bootstrap. */
|
|
41
|
+
exports.CREDENTIAL_REQUEST_TYPE = 'pf-harness-credentials-request';
|
|
42
|
+
/** bootstrap -> harness. */
|
|
43
|
+
exports.CREDENTIAL_RESPONSE_TYPE = 'pf-bootstrap-credentials';
|
|
44
|
+
exports.CREDENTIAL_HANDOFF_TIMEOUT_MS = 15_000;
|
|
45
|
+
/** The variables that may carry a credential. Never left in `process.env`. */
|
|
46
|
+
exports.CREDENTIAL_ENV_KEYS = [
|
|
47
|
+
'PF_ENROLMENT_TOKEN',
|
|
48
|
+
'PF_RUNNER_CREDENTIAL',
|
|
49
|
+
'PF_RUNNER_ID',
|
|
50
|
+
'PF_RUNNER_TENANT_ID',
|
|
51
|
+
'PF_RUNNER_CREDENTIAL_EXPIRES_AT'
|
|
52
|
+
];
|
|
53
|
+
class CredentialHandoffError extends Error {
|
|
54
|
+
constructor(message) {
|
|
55
|
+
super(message);
|
|
56
|
+
this.name = 'CredentialHandoffError';
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
exports.CredentialHandoffError = CredentialHandoffError;
|
|
60
|
+
/** Remove every credential-bearing variable. Idempotent. */
|
|
61
|
+
function scrubCredentialEnv(env) {
|
|
62
|
+
for (const key of exports.CREDENTIAL_ENV_KEYS) {
|
|
63
|
+
delete env[key];
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const nonEmpty = (value) => typeof value === 'string' && value.length > 0;
|
|
67
|
+
function parseResponse(message) {
|
|
68
|
+
if (typeof message !== 'object' || message === null)
|
|
69
|
+
return null;
|
|
70
|
+
const m = message;
|
|
71
|
+
if (m.type !== exports.CREDENTIAL_RESPONSE_TYPE || !nonEmpty(m.enrolmentToken)) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
const credentials = {
|
|
75
|
+
enrolmentToken: m.enrolmentToken
|
|
76
|
+
};
|
|
77
|
+
// A runner id without its credential (or the reverse) is not a handoff.
|
|
78
|
+
if (nonEmpty(m.runnerId) && nonEmpty(m.runnerCredential)) {
|
|
79
|
+
credentials.runnerId = m.runnerId;
|
|
80
|
+
credentials.runnerCredential = m.runnerCredential;
|
|
81
|
+
credentials.tenantId = nonEmpty(m.tenantId) ? m.tenantId : null;
|
|
82
|
+
credentials.credentialExpiresAt = nonEmpty(m.credentialExpiresAt)
|
|
83
|
+
? m.credentialExpiresAt
|
|
84
|
+
: null;
|
|
85
|
+
}
|
|
86
|
+
return credentials;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Ask pf-bootstrap for the credentials over the private IPC channel.
|
|
90
|
+
*
|
|
91
|
+
* Returns null — and does nothing — when the marker is absent: the caller then
|
|
92
|
+
* uses the environment exactly as before. The marker itself is removed either
|
|
93
|
+
* way, so agent subprocesses do not inherit it.
|
|
94
|
+
*/
|
|
95
|
+
async function receiveBootstrapCredentials(proc, env, timeoutMs = exports.CREDENTIAL_HANDOFF_TIMEOUT_MS) {
|
|
96
|
+
const mode = env[exports.CREDENTIAL_HANDOFF_ENV];
|
|
97
|
+
delete env[exports.CREDENTIAL_HANDOFF_ENV];
|
|
98
|
+
if (mode === undefined || mode === '')
|
|
99
|
+
return null;
|
|
100
|
+
if (mode !== exports.CREDENTIAL_HANDOFF_IPC) {
|
|
101
|
+
throw new CredentialHandoffError(`Unsupported ${exports.CREDENTIAL_HANDOFF_ENV} '${mode}'. Expected '${exports.CREDENTIAL_HANDOFF_IPC}'.`);
|
|
102
|
+
}
|
|
103
|
+
const send = proc.send;
|
|
104
|
+
if (typeof send !== 'function') {
|
|
105
|
+
throw new CredentialHandoffError(`${exports.CREDENTIAL_HANDOFF_ENV}=ipc is set but this process has no IPC channel`);
|
|
106
|
+
}
|
|
107
|
+
return new Promise((resolve, reject) => {
|
|
108
|
+
let timer;
|
|
109
|
+
const finish = (error, value) => {
|
|
110
|
+
proc.off('message', onMessage);
|
|
111
|
+
if (timer)
|
|
112
|
+
clearTimeout(timer);
|
|
113
|
+
if (error)
|
|
114
|
+
reject(error);
|
|
115
|
+
else
|
|
116
|
+
resolve(value);
|
|
117
|
+
};
|
|
118
|
+
const onMessage = (message) => {
|
|
119
|
+
const credentials = parseResponse(message);
|
|
120
|
+
if (credentials)
|
|
121
|
+
finish(null, credentials);
|
|
122
|
+
};
|
|
123
|
+
// Listen BEFORE asking, so the answer cannot arrive unheard.
|
|
124
|
+
proc.on('message', onMessage);
|
|
125
|
+
timer = setTimeout(() => finish(new CredentialHandoffError(`pf-bootstrap did not hand over the credentials within ${timeoutMs} ms`)), timeoutMs);
|
|
126
|
+
try {
|
|
127
|
+
send.call(proc, { type: exports.CREDENTIAL_REQUEST_TYPE });
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
finish(new CredentialHandoffError(`Could not ask pf-bootstrap for the credentials: ${error instanceof Error ? error.message : String(error)}`));
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* The credentials as the environment-shaped variables `parseOptions` and
|
|
136
|
+
* `takeHandedOffCredential` already understand. The result is merged into a
|
|
137
|
+
* PLAIN OBJECT copy of the environment, never into `process.env`.
|
|
138
|
+
*/
|
|
139
|
+
function credentialsAsEnv(credentials) {
|
|
140
|
+
const env = {
|
|
141
|
+
PF_ENROLMENT_TOKEN: credentials.enrolmentToken
|
|
142
|
+
};
|
|
143
|
+
if (credentials.runnerId && credentials.runnerCredential) {
|
|
144
|
+
env.PF_RUNNER_ID = credentials.runnerId;
|
|
145
|
+
env.PF_RUNNER_CREDENTIAL = credentials.runnerCredential;
|
|
146
|
+
if (credentials.tenantId)
|
|
147
|
+
env.PF_RUNNER_TENANT_ID = credentials.tenantId;
|
|
148
|
+
if (credentials.credentialExpiresAt) {
|
|
149
|
+
env.PF_RUNNER_CREDENTIAL_EXPIRES_AT = credentials.credentialExpiresAt;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return env;
|
|
153
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
})();
|
|
36
36
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
37
|
const adapter_resolver_1 = require("./adapter-resolver");
|
|
38
|
+
const bootstrap_handoff_1 = require("./bootstrap-handoff");
|
|
38
39
|
const contract_1 = require("./contract");
|
|
39
40
|
const credential_store_1 = require("./credential-store");
|
|
40
41
|
const harness_1 = require("./harness");
|
|
@@ -97,8 +98,8 @@ Options:
|
|
|
97
98
|
Needs Node.js 22.19 or newer; start stops with one line on an older one.
|
|
98
99
|
PF_ALLOW_UNSUPPORTED_NODE=1 starts anyway, at your own risk.
|
|
99
100
|
|
|
100
|
-
In a managed sandbox pf-bootstrap
|
|
101
|
-
|
|
101
|
+
In a managed sandbox pf-bootstrap hands the token over a private IPC channel,
|
|
102
|
+
so it never appears in /proc/<pid>/cmdline or /proc/<pid>/environ.
|
|
102
103
|
`;
|
|
103
104
|
const serviceStart = (0, service_1.isServiceStart)(process.argv.slice(2));
|
|
104
105
|
function stopPermanentServiceFailure(reason, linuxCode) {
|
|
@@ -224,8 +225,22 @@ async function main() {
|
|
|
224
225
|
*/
|
|
225
226
|
const startArgs = argv.slice(1);
|
|
226
227
|
const wantsLogin = startArgs.includes('--login');
|
|
228
|
+
/*
|
|
229
|
+
* CREDENTIALS NEVER STAY IN process.env. A current pf-bootstrap hands them
|
|
230
|
+
* over its private IPC channel (bootstrap-handoff.ts); an older one, a
|
|
231
|
+
* self-hosted install or a developer still uses the environment. Either way
|
|
232
|
+
* they are merged into `startEnv`, a PLAIN OBJECT copy that lives only in
|
|
233
|
+
* this heap, and every credential variable is scrubbed from process.env
|
|
234
|
+
* before anything else runs, so no child can inherit one.
|
|
235
|
+
*/
|
|
236
|
+
const handoff = await (0, bootstrap_handoff_1.receiveBootstrapCredentials)(process, process.env);
|
|
237
|
+
const startEnv = {
|
|
238
|
+
...process.env,
|
|
239
|
+
...(handoff ? (0, bootstrap_handoff_1.credentialsAsEnv)(handoff) : {})
|
|
240
|
+
};
|
|
241
|
+
(0, bootstrap_handoff_1.scrubCredentialEnv)(process.env);
|
|
227
242
|
// `--token=x` counts as a token, hence readOptionValue rather than indexOf.
|
|
228
|
-
const hasToken = Boolean(readOptionValue(startArgs, 'token') ??
|
|
243
|
+
const hasToken = Boolean(readOptionValue(startArgs, 'token') ?? startEnv.PF_ENROLMENT_TOKEN);
|
|
229
244
|
const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
230
245
|
if (!serviceStart && !hasToken && (wantsLogin || interactive)) {
|
|
231
246
|
const { enrolViaBrowser } = await Promise.resolve().then(() => __importStar(require('./enrol/browser-enrol')));
|
|
@@ -239,17 +254,17 @@ async function main() {
|
|
|
239
254
|
appUrl: readOptionValue(startArgs, 'app-url') ?? process.env.PF_APP_URL,
|
|
240
255
|
label: readOptionValue(startArgs, 'label') ?? process.env.PF_RUNNER_LABEL
|
|
241
256
|
});
|
|
242
|
-
// Handed on
|
|
243
|
-
//
|
|
244
|
-
|
|
257
|
+
// Handed on in memory rather than argv: argv is world-readable via
|
|
258
|
+
// /proc/<pid>/cmdline. Never written back to process.env.
|
|
259
|
+
startEnv.PF_ENROLMENT_TOKEN = token;
|
|
245
260
|
}
|
|
246
|
-
const options = (0, options_1.parseOptions)(startArgs,
|
|
261
|
+
const options = (0, options_1.parseOptions)(startArgs, startEnv);
|
|
247
262
|
new service_1.ServiceManager({
|
|
248
263
|
nodePath: process.execPath,
|
|
249
264
|
cliPath: __filename,
|
|
250
265
|
env: process.env
|
|
251
266
|
}).assertNoDuplicateRunner(options);
|
|
252
|
-
const handedOffCredential = (0, credential_store_1.takeHandedOffCredential)(
|
|
267
|
+
const handedOffCredential = (0, credential_store_1.takeHandedOffCredential)(startEnv);
|
|
253
268
|
const resolveAdapter = (0, adapter_resolver_1.createAdapterResolver)(options.runtime, options.workspaceRoot);
|
|
254
269
|
const harness = new harness_1.Harness(options, {
|
|
255
270
|
getAdapter: (backend, ports) => resolveAdapter(backend, ports),
|
|
@@ -81,8 +81,9 @@ export declare class MemoryCredentialStore implements CredentialStore {
|
|
|
81
81
|
clear(): void;
|
|
82
82
|
}
|
|
83
83
|
/**
|
|
84
|
-
* Take the
|
|
85
|
-
* variable before an adapter can copy the
|
|
84
|
+
* Take the bootstrap handoff out of an environment-shaped object and remove
|
|
85
|
+
* every credential-bearing variable from it, before an adapter can copy the
|
|
86
|
+
* process environment to a child.
|
|
86
87
|
*/
|
|
87
88
|
export declare function takeHandedOffCredential(env: NodeJS.ProcessEnv): StoredCredential | null;
|
|
88
89
|
/**
|
package/dist/credential-store.js
CHANGED
|
@@ -39,6 +39,7 @@ exports.takeHandedOffCredential = takeHandedOffCredential;
|
|
|
39
39
|
exports.createCredentialStore = createCredentialStore;
|
|
40
40
|
const node_fs_1 = require("node:fs");
|
|
41
41
|
const node_path_1 = require("node:path");
|
|
42
|
+
const bootstrap_handoff_1 = require("./bootstrap-handoff");
|
|
42
43
|
/** The file name under the workspace root. */
|
|
43
44
|
exports.CREDENTIAL_FILE_NAME = 'runner-credential.json';
|
|
44
45
|
/**
|
|
@@ -80,8 +81,9 @@ class MemoryCredentialStore {
|
|
|
80
81
|
}
|
|
81
82
|
exports.MemoryCredentialStore = MemoryCredentialStore;
|
|
82
83
|
/**
|
|
83
|
-
* Take the
|
|
84
|
-
* variable before an adapter can copy the
|
|
84
|
+
* Take the bootstrap handoff out of an environment-shaped object and remove
|
|
85
|
+
* every credential-bearing variable from it, before an adapter can copy the
|
|
86
|
+
* process environment to a child.
|
|
85
87
|
*/
|
|
86
88
|
function takeHandedOffCredential(env) {
|
|
87
89
|
const runnerId = env.PF_RUNNER_ID;
|
|
@@ -98,11 +100,9 @@ function takeHandedOffCredential(env) {
|
|
|
98
100
|
// On warm upgrade PF_ENROLMENT_TOKEN contains the live runner credential.
|
|
99
101
|
// On cold boot it contains a single-use enrolment token. Neither belongs in
|
|
100
102
|
// an agent subprocess environment after parseOptions() has consumed it.
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
delete env.PF_RUNNER_TENANT_ID;
|
|
105
|
-
delete env.PF_RUNNER_CREDENTIAL_EXPIRES_AT;
|
|
103
|
+
// Scrubbing process.env does NOT clear /proc/<pid>/environ, which is why a
|
|
104
|
+
// current pf-bootstrap hands these over IPC instead (bootstrap-handoff.ts).
|
|
105
|
+
(0, bootstrap_handoff_1.scrubCredentialEnv)(env);
|
|
106
106
|
return stored;
|
|
107
107
|
}
|
|
108
108
|
function isStoredCredential(value) {
|
package/dist/options.d.ts
CHANGED
|
@@ -7,9 +7,10 @@ export declare class OptionsError extends Error {
|
|
|
7
7
|
*
|
|
8
8
|
* NOTE ON THE ENROLMENT TOKEN: it is accepted on the command line for local
|
|
9
9
|
* development and self-hosted installs, which is what the design calls for.
|
|
10
|
-
* In a managed sandbox pf-bootstrap
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* In a managed sandbox pf-bootstrap hands it over its private IPC channel
|
|
11
|
+
* (bootstrap-handoff.ts) and cli.ts passes it here in a plain in-memory copy
|
|
12
|
+
* of the environment, so it never appears in /proc/<pid>/cmdline (world-
|
|
13
|
+
* readable) or /proc/<pid>/environ (readable by the same uid, which Pi
|
|
14
|
+
* shares). An older pf-bootstrap still sets PF_ENROLMENT_TOKEN itself.
|
|
14
15
|
*/
|
|
15
16
|
export declare function parseOptions(argv: string[], env: NodeJS.ProcessEnv): HarnessOptions;
|
package/dist/options.js
CHANGED
|
@@ -182,10 +182,11 @@ function parseSwitch(raw, fallback, name) {
|
|
|
182
182
|
*
|
|
183
183
|
* NOTE ON THE ENROLMENT TOKEN: it is accepted on the command line for local
|
|
184
184
|
* development and self-hosted installs, which is what the design calls for.
|
|
185
|
-
* In a managed sandbox pf-bootstrap
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
185
|
+
* In a managed sandbox pf-bootstrap hands it over its private IPC channel
|
|
186
|
+
* (bootstrap-handoff.ts) and cli.ts passes it here in a plain in-memory copy
|
|
187
|
+
* of the environment, so it never appears in /proc/<pid>/cmdline (world-
|
|
188
|
+
* readable) or /proc/<pid>/environ (readable by the same uid, which Pi
|
|
189
|
+
* shares). An older pf-bootstrap still sets PF_ENROLMENT_TOKEN itself.
|
|
189
190
|
*/
|
|
190
191
|
function parseOptions(argv, env) {
|
|
191
192
|
const controlPlaneUrl = readFlag(argv, 'control-plane') ?? env.PF_CONTROL_PLANE_URL;
|
package/dist/types.d.ts
CHANGED
|
@@ -27,11 +27,15 @@
|
|
|
27
27
|
* can use. `POST /api/coding-agents/sessions/:id/ingest` in particular is
|
|
28
28
|
* identical for all three; that is the whole payoff of the extraction.
|
|
29
29
|
*
|
|
30
|
-
* 4. TOKENS TRAVEL IN
|
|
31
|
-
* `/proc/<pid>/cmdline` is world-readable
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
30
|
+
* 4. TOKENS TRAVEL IN HEADERS OR IN MEMORY, NEVER IN ARGV.
|
|
31
|
+
* `/proc/<pid>/cmdline` is world-readable. `/proc/<pid>/environ` is
|
|
32
|
+
* readable by every process of the same uid — and in a sandbox Pi shares
|
|
33
|
+
* the harness's uid — and keeps the execve snapshot even after
|
|
34
|
+
* `delete process.env.X`. So the RUNNER's own credentials reach a managed
|
|
35
|
+
* sandbox harness over pf-bootstrap's private IPC channel
|
|
36
|
+
* (bootstrap-handoff.ts), not the environment; the environment remains
|
|
37
|
+
* only for a self-hosted or desktop start and an older pf-bootstrap.
|
|
38
|
+
* Do not "improve" this by adding a `--token` flag.
|
|
35
39
|
*/
|
|
36
40
|
export interface HarnessOptions {
|
|
37
41
|
/** workflow-api base url, e.g. https://stage-api.peakflo.ai */
|
package/dist/types.js
CHANGED
|
@@ -28,11 +28,15 @@
|
|
|
28
28
|
* can use. `POST /api/coding-agents/sessions/:id/ingest` in particular is
|
|
29
29
|
* identical for all three; that is the whole payoff of the extraction.
|
|
30
30
|
*
|
|
31
|
-
* 4. TOKENS TRAVEL IN
|
|
32
|
-
* `/proc/<pid>/cmdline` is world-readable
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
31
|
+
* 4. TOKENS TRAVEL IN HEADERS OR IN MEMORY, NEVER IN ARGV.
|
|
32
|
+
* `/proc/<pid>/cmdline` is world-readable. `/proc/<pid>/environ` is
|
|
33
|
+
* readable by every process of the same uid — and in a sandbox Pi shares
|
|
34
|
+
* the harness's uid — and keeps the execve snapshot even after
|
|
35
|
+
* `delete process.env.X`. So the RUNNER's own credentials reach a managed
|
|
36
|
+
* sandbox harness over pf-bootstrap's private IPC channel
|
|
37
|
+
* (bootstrap-handoff.ts), not the environment; the environment remains
|
|
38
|
+
* only for a self-hosted or desktop start and an older pf-bootstrap.
|
|
39
|
+
* Do not "improve" this by adding a `--token` flag.
|
|
36
40
|
*/
|
|
37
41
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
38
42
|
/* -------------------------------------------------------------------------- */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@peakflo/agent-harness",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.13-rc.1",
|
|
4
4
|
"description": "Headless Peakflo coding-agent harness. One codebase, three hosting modes: a Daytona cloud sandbox, a customer VM, and the 20x desktop.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -14,6 +14,9 @@
|
|
|
14
14
|
"README.md",
|
|
15
15
|
"LICENSE"
|
|
16
16
|
],
|
|
17
|
+
"peakflo": {
|
|
18
|
+
"credentialHandoff": "ipc"
|
|
19
|
+
},
|
|
17
20
|
"engines": {
|
|
18
21
|
"node": ">=22.19"
|
|
19
22
|
},
|