@proagentstore/cli 0.4.126 → 0.4.128
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/browser-runner/coding/engine-check.js +4 -1
- package/dist/browser-runner/coding/headless.js +11 -3
- package/dist/browser-runner/coding/launcher-wrapper.js +25 -0
- package/dist/browser-runner/local-artifact/contract.js +7 -1
- package/dist/browser-runner/local-artifact/engine.js +8 -0
- package/dist/browser-runner/local-artifact/runtime.js +21 -2
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { unwrapLauncher } from "./launcher-wrapper.js";
|
|
2
3
|
import { statSync } from "node:fs";
|
|
3
4
|
import { homedir } from "node:os";
|
|
4
5
|
import { join } from "node:path";
|
|
@@ -66,7 +67,9 @@ export function detectEngineLogin(engine, deps = {}) {
|
|
|
66
67
|
*/
|
|
67
68
|
export function checkEngine(input, deps = {}) {
|
|
68
69
|
const engine = (["claude", "gemini", "codex", "grok", "generic"].includes(input.clientType ?? "") ? input.clientType : "claude");
|
|
69
|
-
|
|
70
|
+
// The ENGINE is what must be installed — after a `pas mcp-auth exec --` wrapper (#1006), not the wrapper.
|
|
71
|
+
const command = parseCommand(input.command);
|
|
72
|
+
const parsed = unwrapLauncher(command.bin, command.args);
|
|
70
73
|
const bin = parsed.bin || parseCommand(handlerFor(engine).cliCommand).bin || engine;
|
|
71
74
|
const { binaryPath, via } = resolveEngineSpawn(bin, [], deps);
|
|
72
75
|
const login = input.credentialInjected ? { login: "found", where: "platform" } : detectEngineLogin(engine, deps);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { InputLedger } from "./input-ledger.js";
|
|
2
|
+
import { spawnTarget, unwrapLauncher } from "./launcher-wrapper.js";
|
|
2
3
|
import { runObjectiveBlock } from "./run-objective.js";
|
|
3
4
|
import { spawn } from "node:child_process";
|
|
4
5
|
import { classifyCommand, commandFromToolInput, fillTargetFromResult, toolCallOk, toolResultMark } from "./engine-acts.js";
|
|
@@ -83,6 +84,8 @@ export class HeadlessSession {
|
|
|
83
84
|
mode;
|
|
84
85
|
adapter;
|
|
85
86
|
cmdBin;
|
|
87
|
+
/** A launcher wrapper spawned in front of the engine (`pas mcp-auth exec --`), or empty. */
|
|
88
|
+
cmdPrefix;
|
|
86
89
|
cmdArgs;
|
|
87
90
|
binName;
|
|
88
91
|
/** Wall-clock of the last stdout/stderr byte — drives the persistent-raw idle heuristic. */
|
|
@@ -242,7 +245,10 @@ export class HeadlessSession {
|
|
|
242
245
|
constructor(config) {
|
|
243
246
|
this.config = config;
|
|
244
247
|
this.engineLabel = `${config.clientType}:${config.id}`;
|
|
245
|
-
|
|
248
|
+
// `pas mcp-auth exec -- codex …` (#1006): spawn the wrapper, but drive the engine after `--`.
|
|
249
|
+
const parsed = parseCommand(config.command);
|
|
250
|
+
const { prefix, bin, args } = unwrapLauncher(parsed.bin, parsed.args);
|
|
251
|
+
this.cmdPrefix = prefix;
|
|
246
252
|
// When no explicit command is configured, fall back to THIS engine's default
|
|
247
253
|
// command (codex/gemini/grok/…) — not a hard-coded "claude", which would drive a
|
|
248
254
|
// non-Claude session with the wrong CLI (and mode is already "raw" for it).
|
|
@@ -428,7 +434,8 @@ export class HeadlessSession {
|
|
|
428
434
|
this.buf = "";
|
|
429
435
|
const args = this.adapter.buildLaunchArgs(this.cmdArgs, this.claudeSessionId);
|
|
430
436
|
const env = this.spawnEnv;
|
|
431
|
-
const
|
|
437
|
+
const target = spawnTarget(this.cmdPrefix, this.cmdBin, args);
|
|
438
|
+
const launch = resolveEngineSpawn(target.command, target.args, { env });
|
|
432
439
|
const proc = spawn(launch.command, launch.args, {
|
|
433
440
|
cwd: this.config.workDir,
|
|
434
441
|
env,
|
|
@@ -587,7 +594,8 @@ export class HeadlessSession {
|
|
|
587
594
|
? buildCodexResumeArgs(this.cmdArgs, resumedCodexThreadId, text)
|
|
588
595
|
: this.adapter.buildTurnArgs(this.cmdArgs, text);
|
|
589
596
|
const env = this.spawnEnv;
|
|
590
|
-
const
|
|
597
|
+
const target = spawnTarget(this.cmdPrefix, this.cmdBin, turnArgs);
|
|
598
|
+
const launch = resolveEngineSpawn(target.command, target.args, { env });
|
|
591
599
|
const proc = spawn(launch.command, launch.args, {
|
|
592
600
|
cwd: this.config.workDir,
|
|
593
601
|
env,
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `pas mcp-auth exec -- <engine …>` (#1006, PAS #356) on the runner.
|
|
3
|
+
*
|
|
4
|
+
* The ProAppStore CLI runs a command with the owner's remote PAS MCP credential in that process's
|
|
5
|
+
* environment only. So the runner must SPAWN the wrapper (it is what supplies the token) while
|
|
6
|
+
* everything that understands an engine — the adapter choice (`codex exec` → structured), the turn
|
|
7
|
+
* and resume argument builders, the engine-installed check — works on the ENGINE command after `--`.
|
|
8
|
+
*
|
|
9
|
+
* Matched exactly, as in the API (`workers/api/src/lib/coding-command.ts`) and the SDK
|
|
10
|
+
* (`packages/sdk/src/engine-posture.ts`): `pas` followed by anything other than `mcp-auth exec --`
|
|
11
|
+
* is not a wrapper, and stays the binary.
|
|
12
|
+
*/
|
|
13
|
+
export const PAS_MCP_AUTH_WRAPPER = ["mcp-auth", "exec", "--"];
|
|
14
|
+
export function unwrapLauncher(bin, args) {
|
|
15
|
+
const isPas = (bin.split("/").pop() || "").toLowerCase() === "pas";
|
|
16
|
+
if (!isPas || !PAS_MCP_AUTH_WRAPPER.every((t, k) => args[k] === t) || args.length <= PAS_MCP_AUTH_WRAPPER.length) {
|
|
17
|
+
return { prefix: [], bin, args: [...args] };
|
|
18
|
+
}
|
|
19
|
+
const rest = args.slice(PAS_MCP_AUTH_WRAPPER.length);
|
|
20
|
+
return { prefix: [bin, ...PAS_MCP_AUTH_WRAPPER], bin: rest[0], args: rest.slice(1) };
|
|
21
|
+
}
|
|
22
|
+
/** What to actually spawn: the wrapper (if any) carrying the engine and its built args. */
|
|
23
|
+
export function spawnTarget(prefix, bin, args) {
|
|
24
|
+
return prefix.length ? { command: prefix[0], args: [...prefix.slice(1), bin, ...args] } : { command: bin, args: [...args] };
|
|
25
|
+
}
|
|
@@ -28,6 +28,12 @@ export const LOCAL_ARTIFACT_ENGINES = ["claude", "codex"];
|
|
|
28
28
|
export const LOCAL_ARTIFACT_AUTH_MODES = ["machine", "subscription"];
|
|
29
29
|
export const LOCAL_ARTIFACT_SOURCE_ROLES = ["resume", "profile", "answers"];
|
|
30
30
|
export const REQUIRED_SOURCE_ROLES = ["resume", "profile"];
|
|
31
|
+
/**
|
|
32
|
+
* The owner's saved answers to earlier blocking questions (#1066), sent by PAGS from the owner's
|
|
33
|
+
* Profile and given to the CLI as one more citable source under this role. Not a file source: it
|
|
34
|
+
* never appears in `sources`, `sourceHashes` or a path, and an older runner simply ignores it.
|
|
35
|
+
*/
|
|
36
|
+
export const OWNER_ANSWERS_ROLE = "owner_answers";
|
|
31
37
|
export const LOCAL_ARTIFACT_BLOCK_REASONS = [
|
|
32
38
|
"missing_source",
|
|
33
39
|
"malformed_lead",
|
|
@@ -52,7 +58,7 @@ export const LOCAL_ARTIFACT_EVENT_TYPES = [
|
|
|
52
58
|
];
|
|
53
59
|
export const LOCAL_ARTIFACT_ENGINE_AUTHS = ["machine-login", "subscription", "api-key", "missing_login", "unknown"];
|
|
54
60
|
export const LOCAL_ARTIFACT_PARSE_ATTEMPTS = ["fenced_json", "balanced_object"];
|
|
55
|
-
export const LOCAL_ARTIFACT_CAPS = { questions: 20, questionChars: 300, sources: 6, sourceBytes: 256 * 1024 };
|
|
61
|
+
export const LOCAL_ARTIFACT_CAPS = { questions: 20, questionChars: 300, sources: 6, sourceBytes: 256 * 1024, ownerAnswers: 30, ownerAnswerChars: 500 };
|
|
56
62
|
/** An id used as a folder name: no separators, no dots, nothing a path could be built out of. */
|
|
57
63
|
export const PATH_SEGMENT = /^[A-Za-z0-9_-]{1,100}$/;
|
|
58
64
|
/** A source path inside the workspace: relative, no `..`, no absolute, no NUL, no backslash. */
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
import { mergeEnv } from "../coding/engine-env.js";
|
|
21
21
|
import { resolveEngineAuth } from "../coding/engine-auth.js";
|
|
22
22
|
import { finalText as finalTextFromEngine, missingLogin } from "../local-browser/engine.js";
|
|
23
|
+
import { OWNER_ANSWERS_ROLE } from "./contract.js";
|
|
23
24
|
export { missingLogin };
|
|
24
25
|
const PROVIDER_KEYS = ["ANTHROPIC_API_KEY", "OPENAI_API_KEY"];
|
|
25
26
|
const SUBSCRIPTION_TOKEN = "CLAUDE_CODE_OAUTH_TOKEN";
|
|
@@ -51,6 +52,10 @@ export function buildArtifactEngineSpec(input) {
|
|
|
51
52
|
};
|
|
52
53
|
}
|
|
53
54
|
/** The brief. The rules are stated, but the checks in `checkDraft` are what enforce them. */
|
|
55
|
+
/** The owner's saved answers as one source text: a question and its answer per pair (#1066). */
|
|
56
|
+
export function ownerAnswersText(answers) {
|
|
57
|
+
return answers.map((a) => `Q: ${a.question}\nA: ${a.answer}`).join("\n\n");
|
|
58
|
+
}
|
|
54
59
|
export function tailorPrompt(lead, sources) {
|
|
55
60
|
const l = lead.lead;
|
|
56
61
|
const job = [
|
|
@@ -78,6 +83,9 @@ export function tailorPrompt(lead, sources) {
|
|
|
78
83
|
'- For EVERY factual claim about the owner in either document, add an entry to "claims" with the claim, the source role it comes from, and a short VERBATIM quote copied exactly from that source.',
|
|
79
84
|
"- Every year, email address and phone number you write must appear in the sources.",
|
|
80
85
|
"- Reword and reorder to fit the job; do not add what the sources do not say. Leave out what does not fit.",
|
|
86
|
+
...(sources.some((x) => x.role === OWNER_ANSWERS_ROLE)
|
|
87
|
+
? [`- <source role="${OWNER_ANSWERS_ROLE}"> holds the owner's own answers to questions asked on earlier applications. They are the owner's facts and preferences: use them, cite them like any source, and do not ask again a question they already answer.`]
|
|
88
|
+
: []),
|
|
81
89
|
'- If something the application needs is missing or uncertain, do not guess: answer {"status":"needs_human","questions":["…"]} instead.',
|
|
82
90
|
"",
|
|
83
91
|
"Answer shape when you can do it:",
|
|
@@ -28,8 +28,8 @@ import { join, resolve, sep } from "node:path";
|
|
|
28
28
|
import { RunnerInputError } from "../errors.js";
|
|
29
29
|
import { redactText, secretEnvValues } from "../local-browser/contract.js";
|
|
30
30
|
import { resolveWorkspacePath } from "../local-browser/runtime.js";
|
|
31
|
-
import { LOCAL_ARTIFACT_AUTH_MODES, LOCAL_ARTIFACT_CAPS, LOCAL_ARTIFACT_ENGINES, LOCAL_ARTIFACT_SOURCE_ROLES, LOCAL_ARTIFACT_TASK_TYPE, PATH_SEGMENT, REQUIRED_SOURCE_ROLES, isHomeRelative, isWorkspaceRelative, parseLocalArtifactLead, } from "./contract.js";
|
|
32
|
-
import { buildArtifactEngineSpec, checkDraft, finalText, missingLogin, observedArtifactAuth, parseDraft, tailorPrompt } from "./engine.js";
|
|
31
|
+
import { LOCAL_ARTIFACT_AUTH_MODES, LOCAL_ARTIFACT_CAPS, OWNER_ANSWERS_ROLE, LOCAL_ARTIFACT_ENGINES, LOCAL_ARTIFACT_SOURCE_ROLES, LOCAL_ARTIFACT_TASK_TYPE, PATH_SEGMENT, REQUIRED_SOURCE_ROLES, isHomeRelative, isWorkspaceRelative, parseLocalArtifactLead, } from "./contract.js";
|
|
32
|
+
import { buildArtifactEngineSpec, checkDraft, finalText, missingLogin, observedArtifactAuth, ownerAnswersText, parseDraft, tailorPrompt } from "./engine.js";
|
|
33
33
|
/** The manifest in every generated folder — what retention reads, and proof the folder is ours. */
|
|
34
34
|
export const ARTIFACT_MANIFEST = ".pags-artifact.json";
|
|
35
35
|
const MAX_EVENTS = 200;
|
|
@@ -87,6 +87,18 @@ export function parseArtifactEnvelope(raw) {
|
|
|
87
87
|
if (uploadedSources.length !== 2 || !uploadedSources.some((s) => s.role === "resume") || !uploadedSources.some((s) => s.role === "profile"))
|
|
88
88
|
throw bad("uploaded sources need one resume and one profile");
|
|
89
89
|
}
|
|
90
|
+
// The owner's saved answers to earlier questions (#1066): optional, bounded, plain text only.
|
|
91
|
+
const ownerAnswers = [];
|
|
92
|
+
if (o.ownerAnswers !== undefined) {
|
|
93
|
+
if (!Array.isArray(o.ownerAnswers) || o.ownerAnswers.length > LOCAL_ARTIFACT_CAPS.ownerAnswers)
|
|
94
|
+
throw bad(`ownerAnswers must be a list of at most ${LOCAL_ARTIFACT_CAPS.ownerAnswers}`);
|
|
95
|
+
for (const a of o.ownerAnswers) {
|
|
96
|
+
const r = (a && typeof a === "object" ? a : {});
|
|
97
|
+
if (typeof r.question !== "string" || !r.question.trim() || r.question.length > LOCAL_ARTIFACT_CAPS.questionChars || typeof r.answer !== "string" || !r.answer.trim() || r.answer.length > LOCAL_ARTIFACT_CAPS.ownerAnswerChars)
|
|
98
|
+
throw bad("each owner answer needs a question and an answer within their caps");
|
|
99
|
+
ownerAnswers.push({ question: r.question.trim(), answer: r.answer.trim() });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
90
102
|
const p = (o.policy && typeof o.policy === "object" ? o.policy : {});
|
|
91
103
|
const retainDays = intIn(p.retainDays, 0, 3650);
|
|
92
104
|
const maxMinutes = intIn(p.maxMinutes, 1, 60);
|
|
@@ -104,6 +116,7 @@ export function parseArtifactEnvelope(raw) {
|
|
|
104
116
|
workspace: o.workspace,
|
|
105
117
|
sources,
|
|
106
118
|
...(uploadedSources.length ? { uploadedSources } : {}),
|
|
119
|
+
...(ownerAnswers.length ? { ownerAnswers } : {}),
|
|
107
120
|
policy: { retainDays, maxMinutes, maxConcurrent },
|
|
108
121
|
...("lead" in lead ? { lead: lead.lead } : { lead: null, leadError: lead.error }),
|
|
109
122
|
};
|
|
@@ -235,6 +248,12 @@ export class LocalArtifactRuntime {
|
|
|
235
248
|
}
|
|
236
249
|
if (missing.length)
|
|
237
250
|
throw new Pause("missing_source", missing);
|
|
251
|
+
// The owner's saved answers go in as one more source the CLI may cite (#1066). Not a file, so no
|
|
252
|
+
// source hash or path; the count alone is recorded, never the text.
|
|
253
|
+
if (e.ownerAnswers?.length) {
|
|
254
|
+
texts.push({ role: OWNER_ANSWERS_ROLE, text: ownerAnswersText(e.ownerAnswers) });
|
|
255
|
+
this.emit(run, { type: "source.read", detail: { role: OWNER_ANSWERS_ROLE, count: e.ownerAnswers.length } });
|
|
256
|
+
}
|
|
238
257
|
const outRel = `applications/${lead.leadId}/${e.runId}`;
|
|
239
258
|
const outDir = join(workspace, "applications", lead.leadId, e.runId);
|
|
240
259
|
try {
|