@miller-tech/uap 1.43.3 → 1.44.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/dist/.tsbuildinfo +1 -1
- package/dist/bin/cli.js +2 -0
- package/dist/bin/cli.js.map +1 -1
- package/dist/cli/deliver.d.ts +4 -0
- package/dist/cli/deliver.d.ts.map +1 -1
- package/dist/cli/deliver.js +32 -2
- package/dist/cli/deliver.js.map +1 -1
- package/dist/delivery/self-gate.d.ts +48 -0
- package/dist/delivery/self-gate.d.ts.map +1 -0
- package/dist/delivery/self-gate.js +155 -0
- package/dist/delivery/self-gate.js.map +1 -0
- package/package.json +1 -1
- package/src/policies/enforcers/__pycache__/_common.cpython-312.pyc +0 -0
- package/tools/agents/install-opencode-local.sh.j2 +9 -4
- package/tools/agents/opencode_uap_agent.py +30 -1
- package/tools/agents/plugins/uap-enforce.ts +46 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-authored acceptance gate
|
|
3
|
+
*
|
|
4
|
+
* When a project exposes no detectable gates (no package.json build/test
|
|
5
|
+
* scripts — the common case for polyglot CLI tasks), the convergence loop has
|
|
6
|
+
* nothing to iterate against and degrades to a vacuous single-shot "success".
|
|
7
|
+
*
|
|
8
|
+
* This module asks the executor model to author a task-specific verification
|
|
9
|
+
* script (`.uap-deliver/verify.sh`) that exits 0 iff the task is correctly
|
|
10
|
+
* completed, using only repo-observable evidence (files, program output) — no
|
|
11
|
+
* network, no hidden test harness. The script is then registered as a required
|
|
12
|
+
* gate so deliver converges against it.
|
|
13
|
+
*
|
|
14
|
+
* The floor (anti-vacuous) rule: a usable acceptance gate MUST fail on the
|
|
15
|
+
* current, unsolved repository. If it already passes before the model has done
|
|
16
|
+
* any work, it is trivial and is regenerated with that feedback. This is what
|
|
17
|
+
* stops deliver from "converging to 1 immediately": turn 1 cannot pass a gate
|
|
18
|
+
* that is required to fail at the start.
|
|
19
|
+
*/
|
|
20
|
+
import { spawnSync } from 'child_process';
|
|
21
|
+
import { existsSync, mkdirSync, writeFileSync, chmodSync } from 'fs';
|
|
22
|
+
import { join } from 'path';
|
|
23
|
+
const GATE_DIR = '.uap-deliver';
|
|
24
|
+
const GATE_FILE = 'verify.sh';
|
|
25
|
+
const DEFAULT_ATTEMPTS = 3;
|
|
26
|
+
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
27
|
+
/** Pull a shell script out of a model response (fenced block preferred). */
|
|
28
|
+
export function extractScript(modelOutput) {
|
|
29
|
+
const fence = modelOutput.match(/```(?:bash|sh|shell)?\s*\n([\s\S]*?)```/);
|
|
30
|
+
const body = (fence ? fence[1] : modelOutput).trim();
|
|
31
|
+
// Guarantee a shebang so `bash <file>` and direct exec both behave.
|
|
32
|
+
if (/^#!/.test(body))
|
|
33
|
+
return body;
|
|
34
|
+
return `#!/usr/bin/env bash\n${body}`;
|
|
35
|
+
}
|
|
36
|
+
function buildAuthorPrompt(instruction, projectRoot, priorFeedback) {
|
|
37
|
+
const retry = priorFeedback
|
|
38
|
+
? `\n\nYour previous script was rejected because: ${priorFeedback}\nWrite a stricter script that genuinely verifies the required outcome.`
|
|
39
|
+
: '';
|
|
40
|
+
return [
|
|
41
|
+
'You are writing an ACCEPTANCE TEST for a coding task, not solving it.',
|
|
42
|
+
'',
|
|
43
|
+
`TASK:\n${instruction}`,
|
|
44
|
+
'',
|
|
45
|
+
`PROJECT ROOT: ${projectRoot}`,
|
|
46
|
+
'',
|
|
47
|
+
'Write a self-contained POSIX bash script that:',
|
|
48
|
+
' - exits 0 ONLY if the task is fully and correctly completed',
|
|
49
|
+
' - exits non-zero otherwise (print a short reason to stderr)',
|
|
50
|
+
' - checks concrete, observable evidence: expected files exist, a program',
|
|
51
|
+
' builds/runs, output matches what the task requires',
|
|
52
|
+
' - uses ONLY the repository and standard tools (no network, no access to',
|
|
53
|
+
' any hidden test harness)',
|
|
54
|
+
' - is runnable from the project root',
|
|
55
|
+
'',
|
|
56
|
+
'CRITICAL: the script must FAIL right now, on the current unsolved repo,',
|
|
57
|
+
'and only PASS once the task has actually been done. Do not write a check',
|
|
58
|
+
'that trivially passes (e.g. `exit 0`, or only `test -d .`).',
|
|
59
|
+
'',
|
|
60
|
+
'Output ONLY the script inside a single ```bash code block.',
|
|
61
|
+
retry,
|
|
62
|
+
].join('\n');
|
|
63
|
+
}
|
|
64
|
+
/** Run the candidate gate against the current repo state. */
|
|
65
|
+
function runGate(scriptPath, projectRoot, timeoutMs) {
|
|
66
|
+
const r = spawnSync('bash', [scriptPath], {
|
|
67
|
+
cwd: projectRoot,
|
|
68
|
+
timeout: timeoutMs,
|
|
69
|
+
encoding: 'utf-8',
|
|
70
|
+
env: { ...process.env, CI: 'true' },
|
|
71
|
+
});
|
|
72
|
+
if (r.error) {
|
|
73
|
+
return { exitCode: null, spawnError: true, outputTail: String(r.error.message).slice(-500) };
|
|
74
|
+
}
|
|
75
|
+
const out = `${r.stdout ?? ''}${r.stderr ?? ''}`.slice(-500);
|
|
76
|
+
return { exitCode: r.status, spawnError: false, outputTail: out };
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Author and validate a task-specific acceptance gate. Retries until the
|
|
80
|
+
* generated script fails on the current (unsolved) repo — the non-vacuity
|
|
81
|
+
* floor — or attempts are exhausted.
|
|
82
|
+
*/
|
|
83
|
+
export async function authorAcceptanceGate(opts) {
|
|
84
|
+
const { instruction, projectRoot, executor } = opts;
|
|
85
|
+
const attempts = opts.maxAuthorAttempts ?? DEFAULT_ATTEMPTS;
|
|
86
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
87
|
+
const notes = [];
|
|
88
|
+
const gateDir = join(projectRoot, GATE_DIR);
|
|
89
|
+
const scriptPath = join(gateDir, GATE_FILE);
|
|
90
|
+
if (!existsSync(gateDir))
|
|
91
|
+
mkdirSync(gateDir, { recursive: true });
|
|
92
|
+
let priorFeedback = null;
|
|
93
|
+
let producedAny = false;
|
|
94
|
+
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
95
|
+
let response;
|
|
96
|
+
try {
|
|
97
|
+
response = await executor(buildAuthorPrompt(instruction, projectRoot, priorFeedback));
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
notes.push(`attempt ${attempt}: model error authoring gate`);
|
|
101
|
+
priorFeedback = `the authoring call errored (${String(err).slice(0, 80)})`;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
const script = extractScript(response);
|
|
105
|
+
writeFileSync(scriptPath, script, 'utf-8');
|
|
106
|
+
try {
|
|
107
|
+
chmodSync(scriptPath, 0o755);
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
/* non-fatal */
|
|
111
|
+
}
|
|
112
|
+
producedAny = true;
|
|
113
|
+
const run = runGate(scriptPath, projectRoot, timeoutMs);
|
|
114
|
+
if (run.spawnError) {
|
|
115
|
+
notes.push(`attempt ${attempt}: gate failed to run (${run.outputTail.slice(0, 80)})`);
|
|
116
|
+
priorFeedback = 'the script could not execute (syntax/interpreter error)';
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (run.exitCode === 0) {
|
|
120
|
+
// Vacuous: passes on the unsolved repo. Reject and retry stricter.
|
|
121
|
+
notes.push(`attempt ${attempt}: gate passed on the UNSOLVED repo — too weak, regenerating`);
|
|
122
|
+
priorFeedback = 'it passed on the unsolved repository (it must fail until the work is done)';
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
// Good: a discriminating gate that fails now and must be made to pass.
|
|
126
|
+
notes.push(`attempt ${attempt}: gate fails on unsolved repo (exit ${run.exitCode}) — accepted`);
|
|
127
|
+
return {
|
|
128
|
+
rung: buildRung(scriptPath, timeoutMs),
|
|
129
|
+
scriptPath,
|
|
130
|
+
vacuous: false,
|
|
131
|
+
attempts: attempt,
|
|
132
|
+
notes,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
// Exhausted attempts. Return the last gate (if any) flagged as vacuous/weak so
|
|
136
|
+
// the caller can warn; never silently treat absence as success.
|
|
137
|
+
return {
|
|
138
|
+
rung: producedAny ? buildRung(scriptPath, timeoutMs) : null,
|
|
139
|
+
scriptPath: producedAny ? scriptPath : null,
|
|
140
|
+
vacuous: true,
|
|
141
|
+
attempts,
|
|
142
|
+
notes,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
function buildRung(scriptPath, timeoutMs) {
|
|
146
|
+
return {
|
|
147
|
+
id: 'acceptance',
|
|
148
|
+
name: 'Acceptance check (.uap-deliver/verify.sh)',
|
|
149
|
+
command: 'bash',
|
|
150
|
+
args: [scriptPath],
|
|
151
|
+
required: true,
|
|
152
|
+
timeoutMs,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
//# sourceMappingURL=self-gate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"self-gate.js","sourceRoot":"","sources":["../../src/delivery/self-gate.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
Binary file
|
|
@@ -118,12 +118,17 @@ else
|
|
|
118
118
|
# -------------------------------------------------------------------
|
|
119
119
|
# NPM REGISTRY MODE: Fallback when no local mount is present
|
|
120
120
|
# -------------------------------------------------------------------
|
|
121
|
+
# NOTE: the published package is @miller-tech/uap (the old
|
|
122
|
+
# `universal-agent-protocol` name 404s). Prefer the local /uap-local mount
|
|
123
|
+
# (handled above + re-installed by the agent after upload); this registry
|
|
124
|
+
# path is only a last-resort fallback.
|
|
121
125
|
CURRENT_UAP=$(uap --version 2>/dev/null || echo "none")
|
|
122
|
-
if [ "$CURRENT_UAP"
|
|
123
|
-
echo " UAP v${
|
|
126
|
+
if [ "$CURRENT_UAP" != "none" ]; then
|
|
127
|
+
echo " UAP already installed (v${CURRENT_UAP}) - skipping registry fetch"
|
|
124
128
|
else
|
|
125
|
-
npm i -g
|
|
126
|
-
|
|
129
|
+
npm i -g @miller-tech/uap@latest 2>/dev/null \
|
|
130
|
+
&& echo " UAP installed from npm registry (@miller-tech/uap@latest)" \
|
|
131
|
+
|| echo " WARNING: UAP npm registry install failed (will rely on /uap-local)"
|
|
127
132
|
fi
|
|
128
133
|
fi
|
|
129
134
|
|
|
@@ -1179,6 +1179,10 @@ class OpenCodeUAP(BaseInstalledAgent):
|
|
|
1179
1179
|
"tools/agents",
|
|
1180
1180
|
"tools/uap_harbor",
|
|
1181
1181
|
"harbor-configs",
|
|
1182
|
+
# Needed so `uap install opencode` can wire the policy gate +
|
|
1183
|
+
# delivery-enforcement enforcer inside the container.
|
|
1184
|
+
"src/policies/enforcers",
|
|
1185
|
+
"src/policies/schemas/policies",
|
|
1182
1186
|
]
|
|
1183
1187
|
local_upload_files = [
|
|
1184
1188
|
"package.json",
|
|
@@ -1227,6 +1231,13 @@ class OpenCodeUAP(BaseInstalledAgent):
|
|
|
1227
1231
|
|
|
1228
1232
|
logger.info("[Local UAP] Local project uploaded to /uap-local/")
|
|
1229
1233
|
|
|
1234
|
+
# NOTE: we deliberately do NOT `npm i -g file:/uap-local` here. The
|
|
1235
|
+
# install script's npm path already globally installs @miller-tech/uap
|
|
1236
|
+
# (with its runtime deps) and runs `uap install opencode`. Re-linking
|
|
1237
|
+
# the global bin to /uap-local — which has no node_modules — would break
|
|
1238
|
+
# the CLI at runtime (ERR_MODULE_NOT_FOUND: commander). The uploaded
|
|
1239
|
+
# /uap-local copy is kept only for Python-side imports / inspection.
|
|
1240
|
+
|
|
1230
1241
|
def populate_context_post_run(self, context: AgentContext) -> None:
|
|
1231
1242
|
_parse_token_counts(self.logs_dir, context)
|
|
1232
1243
|
|
|
@@ -1398,7 +1409,14 @@ class OpenCodeUAP(BaseInstalledAgent):
|
|
|
1398
1409
|
def create_run_agent_commands(self, instruction: str) -> list[ExecInput]:
|
|
1399
1410
|
model = self.model_name or "llama.cpp/qwen35-a3b-iq4xs"
|
|
1400
1411
|
|
|
1401
|
-
env = {
|
|
1412
|
+
env = {
|
|
1413
|
+
"OPENCODE_FAKE_VCS": "git",
|
|
1414
|
+
# Transparent delivery: the uap-enforce plugin reads these to run
|
|
1415
|
+
# the `uap deliver` convergence loop on the first source edit.
|
|
1416
|
+
"UAP_DELIVER_ENDPOINT": self._api_endpoint,
|
|
1417
|
+
"UAP_DELIVER_MODEL": os.environ.get("UAP_DELIVER_MODEL", "qwen35-a3b"),
|
|
1418
|
+
"UAP_ENFORCE_DELIVERY": os.environ.get("UAP_ENFORCE_DELIVERY", "block"),
|
|
1419
|
+
}
|
|
1402
1420
|
|
|
1403
1421
|
# --- Step 0: Build classified CLAUDE.md and enhanced instruction ---
|
|
1404
1422
|
classified_claude_md = build_classified_claude_md(instruction)
|
|
@@ -1524,6 +1542,17 @@ class OpenCodeUAP(BaseInstalledAgent):
|
|
|
1524
1542
|
# --- Step 5: Environment bootstrapping ---
|
|
1525
1543
|
commands.append(ExecInput(command=ENV_BOOTSTRAP_CMD))
|
|
1526
1544
|
|
|
1545
|
+
# --- Step 5b: Stage raw task instruction for transparent delivery ---
|
|
1546
|
+
# The uap-enforce plugin reads /app/.uap-deliver/task.txt to seed the
|
|
1547
|
+
# `uap deliver` convergence loop on the first source edit.
|
|
1548
|
+
task_b64 = base64.b64encode(instruction.encode()).decode()
|
|
1549
|
+
deliver_task_cmd = (
|
|
1550
|
+
"mkdir -p /app/.uap-deliver && "
|
|
1551
|
+
f"echo '{task_b64}' | base64 -d > /app/.uap-deliver/task.txt && "
|
|
1552
|
+
"echo '[Deliver] task instruction staged for transparent delivery'"
|
|
1553
|
+
)
|
|
1554
|
+
commands.append(ExecInput(command=deliver_task_cmd))
|
|
1555
|
+
|
|
1527
1556
|
# --- Step 6: Run opencode with enhanced instruction ---
|
|
1528
1557
|
# opencode.json baseURL points to proxy at http://127.0.0.1:11435/v1
|
|
1529
1558
|
# which injects tool_choice="required" and forwards to the real LLM
|
|
@@ -41,6 +41,11 @@ export const UapEnforce: Plugin = async ({ $ }) => {
|
|
|
41
41
|
let totalToolCalls = 0;
|
|
42
42
|
const SOFT_BUDGET = 30;
|
|
43
43
|
|
|
44
|
+
// Transparent delivery: the first substantive source edit triggers the
|
|
45
|
+
// `uap deliver` convergence loop ONCE per session. Guarded so it never
|
|
46
|
+
// re-enters or fires per-edit.
|
|
47
|
+
let deliverRan = false;
|
|
48
|
+
|
|
44
49
|
const TELEMETRY_PATH = '/tmp/uap-telemetry.jsonl';
|
|
45
50
|
|
|
46
51
|
const simpleHash = (s: string): string => {
|
|
@@ -60,6 +65,47 @@ export const UapEnforce: Plugin = async ({ $ }) => {
|
|
|
60
65
|
'tool.execute.before': async (input, output) => {
|
|
61
66
|
totalToolCalls++;
|
|
62
67
|
|
|
68
|
+
// --- Transparent delivery (Layer 5): converge via `uap deliver` ---
|
|
69
|
+
// When delivery enforcement is active (the UAP default), the FIRST
|
|
70
|
+
// substantive write/edit hands the task to the `uap deliver` convergence
|
|
71
|
+
// loop, which iterates the model against the project's real gates
|
|
72
|
+
// (build/typecheck/test) before the agent's own edit lands. This is how
|
|
73
|
+
// deliver "uplifts" a small local model: plausible -> verified. It runs
|
|
74
|
+
// exactly once per session; the original edit then proceeds normally.
|
|
75
|
+
// Escape hatches: UAP_ENFORCE_DELIVERY=advisory or UAP_DELIVER_BYPASS=1.
|
|
76
|
+
if (
|
|
77
|
+
!deliverRan &&
|
|
78
|
+
(input.tool === 'write' || input.tool === 'edit') &&
|
|
79
|
+
process.env.UAP_ENFORCE_DELIVERY !== 'advisory' &&
|
|
80
|
+
!process.env.UAP_DELIVER_BYPASS
|
|
81
|
+
) {
|
|
82
|
+
deliverRan = true; // set first — prevents re-entry / per-edit loops
|
|
83
|
+
try {
|
|
84
|
+
const endpoint = process.env.UAP_DELIVER_ENDPOINT || '';
|
|
85
|
+
const model = process.env.UAP_DELIVER_MODEL || 'qwen35-a3b';
|
|
86
|
+
const endpointArg = endpoint ? `--endpoint ${endpoint}` : '';
|
|
87
|
+
const deliverScript =
|
|
88
|
+
'source $HOME/.nvm/nvm.sh 2>/dev/null || true; ' +
|
|
89
|
+
'TASK="$(cat /app/.uap-deliver/task.txt 2>/dev/null)"; ' +
|
|
90
|
+
'if [ -n "$TASK" ] && command -v uap >/dev/null 2>&1; then ' +
|
|
91
|
+
`echo "[Deliver] converging via uap deliver (model ${model})..."; ` +
|
|
92
|
+
`UAP_DELIVER_ACTIVE=1 uap deliver "$TASK" ${endpointArg} --model ${model} ` +
|
|
93
|
+
'--project-root /app --max-turns 5 --no-until-delivered ' +
|
|
94
|
+
'> /logs/agent/uap-deliver.log 2>&1 || true; ' +
|
|
95
|
+
'echo "[Deliver] convergence loop finished"; ' +
|
|
96
|
+
'else echo "[Deliver] skipped (no task file or uap CLI absent)"; fi';
|
|
97
|
+
await $`bash -lc ${deliverScript}`.nothrow();
|
|
98
|
+
await $`echo ${JSON.stringify(
|
|
99
|
+
JSON.stringify({ event: 'transparent_deliver', tool: input.tool, ts: new Date().toISOString() })
|
|
100
|
+
)} >> ${TELEMETRY_PATH}`
|
|
101
|
+
.quiet()
|
|
102
|
+
.nothrow();
|
|
103
|
+
} catch {
|
|
104
|
+
/* fail open — let the original edit proceed */
|
|
105
|
+
}
|
|
106
|
+
// fall through: the agent's own edit still executes
|
|
107
|
+
}
|
|
108
|
+
|
|
63
109
|
// --- Worktree enforcement (Layer 4) ---
|
|
64
110
|
// Warn when file writes happen outside a worktree
|
|
65
111
|
if ((input.tool === 'write' || input.tool === 'edit') && output.args?.filePath) {
|