Add verified provider update preflight and record first self-host trial

This commit is contained in:
thePR0M3TH3AN committed 2026-09-30 14:48:41 -04:00
1 parent 2a8ec35dac
commit f72daadcba
17 files changed
+855 -22

No files matched your search

+50 -1
View File
@@ -69,11 +69,60 @@
".torch/backlog",
".torch/prompts"
],
"patched": [],
"patched": [
{
"type": "git-info-exclude-line",
"path": "/home/user/Documents/GitHub/TORCH/.git/info/exclude",
"line": ".task",
"installationId": "73b013e7-41d2-493b-a3a7-022c086f059e"
}
],
"external": [
{
"type": "local-state",
"path": "/home/user/.local/share/torch/projects/3dc11932-6e3c-42aa-9a8a-ca7b8f818b54"
},
{
"type": "worktree",
"area": "session-manager",
"path": "/home/user/TORCHWorktrees/3dc11932-6e3c-42aa-9a8a-ca7b8f818b54/session-manager",
"branch": "torch/session-manager"
},
{
"type": "worktree",
"area": "project-kernel",
"path": "/home/user/TORCHWorktrees/3dc11932-6e3c-42aa-9a8a-ca7b8f818b54/project-kernel",
"branch": "torch/project-kernel"
},
{
"type": "worktree",
"area": "provider-runtime",
"path": "/home/user/TORCHWorktrees/3dc11932-6e3c-42aa-9a8a-ca7b8f818b54/provider-runtime",
"branch": "torch/provider-runtime"
},
{
"type": "worktree",
"area": "work-integration",
"path": "/home/user/TORCHWorktrees/3dc11932-6e3c-42aa-9a8a-ca7b8f818b54/work-integration",
"branch": "torch/work-integration"
},
{
"type": "worktree",
"area": "owner-console",
"path": "/home/user/TORCHWorktrees/3dc11932-6e3c-42aa-9a8a-ca7b8f818b54/owner-console",
"branch": "torch/owner-console"
},
{
"type": "worktree",
"area": "release-self-host",
"path": "/home/user/TORCHWorktrees/3dc11932-6e3c-42aa-9a8a-ca7b8f818b54/release-self-host",
"branch": "torch/release-self-host"
},
{
"type": "worktree",
"area": "qa",
"path": "/home/user/TORCHWorktrees/3dc11932-6e3c-42aa-9a8a-ca7b8f818b54/qa",
"branch": "torch/qa"
}
]
}
+8 -7
View File
@@ -32,6 +32,7 @@
"runtimes": {
"default": "codex",
"codex": {
"updatePolicy": { "mode": "auto", "max_age_hours": 24 },
"model": "gpt-6-luna",
"reasoning": "high",
"launchPolicy": {
@@ -237,8 +238,8 @@
],
"resources": [],
"runtime": "codex",
"model": null,
"reasoning": null,
"model": "gpt-5.6-terra",
"reasoning": "high",
"branch": null,
"worktree_name": null
},
@@ -279,8 +280,8 @@
],
"resources": [],
"runtime": "codex",
"model": null,
"reasoning": null,
"model": "gpt-5.6-terra",
"reasoning": "high",
"branch": null,
"worktree_name": null
},
@@ -322,8 +323,8 @@
],
"resources": [],
"runtime": "codex",
"model": null,
"reasoning": null,
"model": "gpt-5.6-terra",
"reasoning": "high",
"branch": null,
"worktree_name": null
},
@@ -479,7 +480,7 @@
"browser"
],
"runtime": "codex",
"model": "gpt-6-luna",
"model": "gpt-5.6-terra",
"reasoning": "high",
"branch": null,
"worktree_name": null
+31
View File
@@ -164,6 +164,37 @@ Requirements:
- Node.js 22 or later
- Git
### Provider version preflight
Opt in during installation with `torch install ... --update-runtimes --yes`,
or on an existing project with `torch runtimes update-policy --mode auto --yes`.
Approved `torch up --yes` (including resume) refreshes the selected built-in
providers against the official npm `latest` release, at most once every 24
hours. Change the interval with `--max-age-hours 1` through `168`; disable future
downloads with `--mode off`. Dry runs never query the registry or install code.
Existing projects stay opt-in; `--yes` alone does not authorize new downloads.
Preview or force a manual update:
```bash
torch runtimes update --runtime codex,claude,pi --dry-run --json
torch runtimes update --runtime codex,claude,pi --force --yes --json
```
Updates install exact resolved versions into XDG-local TORCH-owned prefixes,
verify `--version` and `--help`, then atomically select their absolute executable
paths. Global installations, login files and provider settings are untouched;
old versions remain available to already-running processes. Codex and Claude
packages can execute installation scripts; Pi uses its current official
`@earendil-works/pi-coding-agent` distribution with scripts disabled. Custom
adapters require their own reviewed updater. Concurrent updates stop at a lock;
failed downloads or verification retain the prior selection and block automatic
startup rather than silently claiming the latest version. Interrupted updates
leave the lock for inspection, not automatic deletion. Model/account access and
adapter flag compatibility remain separate live qualification gates. Persistent
schedule and hierarchy-start updater integration remains pending; use `torch up`
for this first trial.
Run the current verification suite:
```bash
+181
View File
@@ -1225,6 +1225,187 @@ ownership unless it is represented by an allowed durable operation.
## 14. Runtime adapter contract
### 14.0 Provider distribution freshness
Owners MAY approve automatic built-in provider updates at installation with
`--update-runtimes`, or later through `torch runtimes update-policy --mode auto
--yes`. Without that policy, installation/startup MUST NOT silently acquire
new code. Approved normal `torch up --yes`, including resume, MUST refresh the
selected providers against their official npm latest stable releases when the
last successful check is older than the configured 1–168-hour window (24 hours
by default). Preview MUST be offline. Codex, Claude Code and Pi MUST be covered;
custom adapters require a separately reviewed distribution strategy.
TORCH MUST use versioned machine-local prefixes and exact selected executable
paths, not assume the first PATH entry is current. Downloads MUST preserve
global installs, login state, running processes and user provider settings.
The updater MUST serialize writes, validate registry versions, verify the
installed version and parser startup, and atomically publish each provider's
activation record only after success. Failed or interrupted updates MUST NOT
silently fall back while claiming freshness; prior activations and failed
installation evidence remain available. This does not establish model access,
live adapter compatibility, update-script safety or off-machine recoverability.
Package lifecycle execution MUST be visible in the approval preview. Persistent
schedule/hierarchy launch paths and platform-specific installers require their
own qualification; initial automatic integration is the normal `up` path.
### 14.0a Traceable production pipelines (planned)
TORCH MUST support optional project-specific multi-stage production flows,
without hard-coding game-art stages or requiring a pipeline in simple projects.
Session Architect and managers SHOULD identify recurring handoff failures,
repeated stage sequences, approval waits and integrated deliverables as evidence
that a pipeline may help. They MUST propose, not silently activate, a definition.
Prefer a simple checklist where no durable multi-stage state is justified.
A pipeline is a versioned contract for producing a deliverable; it is neither
a second backlog nor a mandatory new manager. Each definition MUST describe:
- a concrete outcome and one accountable flow coordinator;
- a bounded stage DAG with explicit parallel stages and join prerequisites;
- existing specialist implementation owners for each stage;
- required input types, immutable versions/hashes, and output contracts;
- project invariants, applicable checks, test conditions and scarce resources;
- machine acceptance criteria separately from creative/owner approvals;
- retry safety, failure escalation, rework routes and stall thresholds;
- permitted model/tool profiles and separate spending/asset-generation approval;
- external dependencies and honest reproducibility limitations.
Project constraints such as zero stock primitives, a locked model sheet or an
exact UV template MUST be explicit stage invariants. They MUST NOT become
generic defaults or be relaxed because a downstream tool finds them inconvenient.
Human taste approval MUST NOT be replaced by an automated geometry gate.
Each deliverable MUST have one persistent pipeline-instance record (for example
one rig, character, voice pack, migration, research packet or release). The record
MUST pin the definition revision and identify its current accepted outputs,
stage attempts, responsible identities, existing backlog task IDs, dependencies,
receipts, approval revisions, provenance and unresolved waits. Stage work MUST
use the existing authoritative backlog. A pipeline card is a view of that work
and its deliverable lineage, not another independently assignable task system.
A stage becomes eligible only when all predecessor outputs match their pinned
contracts and required checks/approvals are valid. Running, output-produced,
machine-verified, awaiting-approval, accepted, failed and invalidated MUST remain
distinguishable. Task completion or a file's existence MUST NOT imply acceptance.
Advancement MUST use revision checks so duplicate handoffs, parallel attempts
and concurrent managers cannot advance the same instance twice.
Changed upstream content MUST invalidate dependent evidence and approvals, not
silently reuse them. A definition edit MUST NOT rewrite a running instance's
history. Explicit owner-reviewed migration or a new versioned instance is
required. Rework MUST retain the rejected outputs, reasons and attempt lineage;
rollback MUST preserve history and respect irreversible external effects.
Provenance MUST include exact code commits where relevant, artifact content
digests, reference/UV-map/model-sheet versions, tool/model versions, non-secret
generation parameters and check conditions. Prompts and resource-sensitive
metadata MUST remain private with redaction and deliberate export. Seeds alone
MUST NOT be represented as guaranteeing reproducibility of hosted image/voice
models. Generated outputs are immutable recorded evidence; regeneration is a
new attempt, even with identical inputs. No generative provider or chargeable
service is invoked merely because a definition names it.
Debugging MUST answer: what is waiting, why, on whom, since when, which input or
gate failed, what changed between attempts, and what exact evidence permitted
the last transition. Evidence SHOULD support before/after visual review where
useful. Checks MAY be reused through explicit input/output capability contracts;
TORCH MUST NOT claim a mesh/UV/transcription checker exists just because it is
named in a template. Each project supplies and qualifies domain-specific checks.
Managers MUST surface stalled instances and handoff waits even when individual
tasks have recent commits. Waiting on owner, specialist, check resource, external
service and upstream rework MUST be separate reasons, not one generic stale
clock. Manager direct-report check-ins SHOULD consult these exception views.
They MUST NOT approve their own outputs, create approval pressure loops, or
invent tasks to keep a pipeline busy.
The Console SHOULD show deliverable cards, stage timeline/DAG, accepted versions,
current owner, age/reason of waits, failed checks, approval actions and evidence.
Owners MUST be able to comment on an exact output version, approve within their
authority and request rework. The hierarchy coordinates outcomes; specialists
retain ownership and direct communication. Existing Art/Production leads MAY
coordinate a flow; a new lead requires the existing evidence/owner approval path.
Initial implementation order (not implemented by this specification update):
Console design requirements:
- Show a Pipelines navigation entry only when approved definitions/instances
exist. Otherwise show a contextual proposal entry in setup, not an empty
production board or an automatically created generic pipeline.
- Group instances by pipeline revision. Each row shows deliverable name,
current stage(s), coordinator, stage owner, working/waiting duration, waiting
reason/party, latest evidence and attention status. A pipeline-stage strip
communicates actual accepted, active, waiting and invalidated stages. Parallel
active stages MUST NOT be flattened into a falsely linear progress percentage.
- Filter by pipeline, deliverable, coordinator, stage owner, wait reason and
attention state. Save views through the existing project-scoped view mechanism.
A linked backlog task can open its deliverable view; ordinary tasks remain on
the Work board. Show a reciprocal link instead of copying task state.
- Opening a deliverable shows its actual DAG/timeline, inputs and output
previews, acceptance metrics, approval subject/revision, rejection reason,
attempt history and upstream/downstream lineage. A "Why is this waiting?"
summary identifies the exact unmet prerequisite and permitted next action.
- Owner actions use the existing preview/confirmation and authority boundaries:
comment on an exact artifact, review an approval, request rework, pause or
resume a permitted instance, or review a proposed definition migration.
No drag gesture may bypass a stage gate; raw state editing is not an owner
convenience feature. A definition/role proposal is never a creation approval.
- Retain the existing dark TORCH visual identity, keyboard accessibility,
mobile layout, draft/focus preservation and protected refresh. Text/icons
must distinguish stage states without relying on color. On mobile show the
current stage and blocker first, then expand the full flow. Failed snapshot
loads preserve the last evidence with an explicit stale timestamp.
- Aggregate per-stage active versus waiting time, rework count, categorized
check/approval failures, measured usage/cost and evidence coverage. Unknown
cost remains unknown; wall-clock time is not active processing time. Compare
metrics only for compatible definition versions and explain denominators.
- After a configurable sample (five completed instances is a starting point,
not a universal rule), a manager MAY propose one bottleneck improvement with
baseline, expected metric change and review window. Changes require the same
definition-review boundary; existing instances stay pinned. Avoid claiming
causal improvements from small or incompatible samples.
- The shared Console demo MUST use the same renderer with clearly labelled
fixture evidence. It MUST NOT call live provider APIs or represent fixture
approvals and metrics as real project measurements.
Example only (project-defined stages, not a built-in rig template):
```text
Pipelines / Rig production
Deliverable Stage Waiting on Since Owner
Bastion Model sheet review Art approval 2 days Concept Art
Wayfarer Texture check UV coverage 18 min Textures
Bastion / definition v2 / attempt 3
Spec -> Concept -> Model sheet -> Mesh -> Texture -> Integration
awaiting approval
Inputs: approved concept digest / locked proportions digest
Output: model-sheet digest + preview
Required: proportion check passed; Art Director approval pending
[Review evidence] [Comment] [Request rework]
History: attempt 2 rejected, silhouette too wide; retained for comparison
```
This view is planned, not a currently available Console feature.
1. Definition/instance schemas, validation, read-only pipeline assessment and
owner-reviewed activation; reject cycles, unknown owners/checks and overlaps.
2. Backlog-linked durable transitions, pinned artifacts, checks/approvals and
output-specific feedback, with rework and idempotent crash recovery.
3. Why-waiting and stall diagnostics plus manager exception reporting.
4. Console deliverable cards and attempt/lineage comparison.
5. TORCH self-host pilot on a docs/software pipeline; COMBATRIG art integration
remains a separate project-authorized adoption.
Required acceptance scenarios MUST prove that a stale UV/reference input blocks
downstream advancement; changed output invalidates approval; duplicate handoffs
advance once; a parallel join waits for every accepted branch; rejection preserves
history; restart resumes the same stage attempt; resource waits are not mistaken
for productive work; and an owner decision cannot be replaced by an AI manager.
No pipeline runtime or COMBATRIG mutation is authorized by documenting this plan.
Each adapter SHOULD implement capabilities equivalent to:
```text
+42
View File
@@ -2195,3 +2195,45 @@ test_integrity_note:
did_relax_any_assertion: false
if_true_explain_spec_basis: ""
```
# Provider update preflight (2026-09-30)
Self-host packaging follow-up: extended the existing SCN-candidate-isolation
fixture with a tracked `.torch/torch.yaml`; assert it is absent from the engine
candidate while committed engine files and dependency symlinks remain present.
This adds a stricter no-project-state-shipping invariant without changing any
existing assertion. Update policy is validated under `runtimes.<name>.updatePolicy`,
which older readers already treat as extensible runtime data; no incompatible
top-level project key or permanent global provider setting is introduced.
```yaml
test_integrity_note:
change_type: new_tests
scenarios:
- id: SCN-provider-updates
given: approved selected built-in providers and deterministic registry
when: latest versions install and pass parser/version smoke checks
then: registry launches exact managed binaries and fresh startup is offline
- id: SCN-provider-update-cli
given: installed project with owner-enabled update policy
when: dry run, actual startup and failed stale refresh execute
then: preview is offline, actual launch uses updated binary, failure cannot launch
- id: SCN-provider-updates-recovery
given: prior verified activation
when: download fails, another updater owns lock or executable changes
then: prior selection survives failure and unsafe or concurrent use is refused
- id: SCN-provider-update-safety
given: fixed official package allowlist and owned prefixes
when: arbitrary name, injected version, escaped prefix or symlink store appears
then: updater refuses without arbitrary install execution
observable_outcomes:
- persisted selected versions and exact launch commands
- refusal codes and absence of downloads during preview or fresh-cache use
determinism_controls:
- isolated XDG roots, local Git fixtures and boundary installer runner
- no provider accounts or live registry in regression tests
anti_cheat_rationale:
prevents: [unverified activation, global overwrite, silent stale fallback, arbitrary command injection]
relaxation:
did_relax_any_assertion: false
if_true_explain_spec_basis: ""
```
+55
View File
@@ -1,5 +1,60 @@
# TORCH remaining work
## First real self-host run (2026-09-30)
### Portable production pipelines (new roadmap element)
- [ ] Detect justified recurring multi-stage flows from repo/spec and handoff
evidence; propose owner-reviewed pipeline definitions without auto-creation.
- [ ] Define versioned stage DAGs with one flow coordinator, specialist owners,
pinned inputs/outputs, project invariants, checks, resources and approval roles.
- [ ] Track deliverable instances through the existing backlog/artifact/check/
approval systems; do not create another independently scheduled task queue.
- [ ] Implement revision-checked transitions, provenance, attempt lineage,
invalidation, parallel joins, rework and idempotent interrupted-stage recovery.
- [ ] Add why-waiting/stall diagnostics and manager exception check-ins for
handoffs and deliverables, not just stale task commits.
- [ ] Add Console deliverable cards, stage timeline, exact-output feedback,
approval/rework actions and before/after evidence.
- [ ] Add conditional Pipelines navigation, per-definition deliverable rows,
reciprocal Work-board links, why-waiting drilldown, parallel stage view,
accessible mobile/current-blocker presentation and protected refresh.
- [ ] Add evidence-backed stage metrics and configurable sample-based reviews;
keep unknown costs explicit and preserve definition-version comparisons.
Use the same renderer for private live state and clearly labelled demo fixtures.
- [ ] Pilot a TORCH software/docs pipeline, then separately qualify project-
supplied mesh, UV-paintback, transcription or other domain checks as needed.
Canonical contract/acceptance plan: spec section 14.0a. No runtime implemented yet.
- [x] Install accepted `0.1.0-alpha.0` into the normal version store, activate
stable launcher, install six-domain configuration and create seven worktrees.
- [x] Verify ChatGPT sign-in, Luna specialist startup and Sol 6.1 manager startup
using the current installed Codex 0.159.2 binary. Durable specialist message,
manager acknowledgement and runtime IDs observed. No development dispatch,
pushes, deployments or host timers enabled.
- [x] Add owner-approved install/startup provider version preflight for Codex,
Claude Code and Pi: exact managed binaries, 24-hour freshness, serialized
updates, retained previous installs, offline previews and fail-closed errors.
Four deterministic scenarios added; live download qualification recorded in
the first-run audit, not implied by mocked installer tests.
- [ ] Fix `torch brief` invoked from a managed worktree: both live sessions hit
`LOCAL_STATE_METADATA_MISMATCH`. Resolve canonical project binding with
checked worktree membership, never weaken root/installation identity checks.
- [ ] Surface Codex JSON stdout failure events: first Sol launch hid the actual
model rejection behind an unrelated inherited Vercel MCP authentication error.
- [ ] Add exact executable/version/model/auth preflight to owner Console and
setup flow. PATH selected CLI 0.158.0 while the current session used 0.159.2;
the older child rejected Sol 6.1 and required manual investigation/retry.
- [ ] Make configuration checkpoint/worktree/manifest handoffs explicit or
guided: generated tracked files and provisioning manifest updates require
deliberate commits; a healthy doctor does not prove startup readiness.
- [ ] Isolate irrelevant inherited global MCP servers without removing user
settings. Vercel auth noise appeared in both failed and successful Codex runs.
- [ ] Integrate approved update policy into hierarchy activation and persistent
schedule wakes; qualify interrupted-update recovery and Windows npm execution.
- [ ] Qualify current Pi flag/extension/login compatibility after its official
package migration; a verified binary `--help` is not a full adapter proof.
## Review corrections (2026-09-29)
## Fresh readiness audit (2026-09-30)
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "torch-agent-fleet",
"version": "0.1.0-alpha.0",
"version": "0.1.0-alpha.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "torch-agent-fleet",
"version": "0.1.0-alpha.0",
"version": "0.1.0-alpha.1",
"bundleDependencies": [
"@modelcontextprotocol/server",
"typebox",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "torch-agent-fleet",
"version": "0.1.0-alpha.0",
"version": "0.1.0-alpha.1",
"type": "module",
"description": "TORCH — a portable, provider-independent AI development fleet for Git repositories",
"main": "src/cli.mjs",
@@ -0,0 +1,114 @@
# TORCH first real installation and startup
## Outcome
The tool is installed into TORCH itself. The accepted alpha.0 engine and stable
launcher were activated, six specialist domains plus Session Manager received
isolated worktrees, and ChatGPT-authenticated Codex turns ran for Owner Console
(Luna/high) and Session Manager (Sol 6.1/high). The specialist sent a durable
message; the manager acknowledged it and sent a reply. Both durable runtime IDs
were captured. Empty backlog and idle are preserved; no development dispatched.
This is an installation/communication smoke test, not full release qualification.
Both sessions reported a worktree CLI binding problem rather than bypassing it.
No provider API key was read or installed. No pushes, deployment, COMBATRIG
changes, persistent host timers or automatic claims were enabled.
## Configuration
- Project ID: `3dc11932-6e3c-42aa-9a8a-ca7b8f818b54`.
- Launcher: `/home/user/.local/bin/torch`.
- Project state: `.torch/` plus XDG-local project history.
- Worktrees: `/home/user/TORCHWorktrees/3dc11932-6e3c-42aa-9a8a-ca7b8f818b54/`.
- Session Manager: `gpt-6.1-sol`, high.
- Kernel, Runtime, Work/Integration and QA: `gpt-5.6-terra`, high, not launched.
Interpreted "Tera" as the account's exact Terra model; owner clarification
can change those project profiles without code/global-settings changes.
- Owner Console and Release/Self-host: `gpt-6-luna`, high.
- Proposed domain leads remain inactive; this was not a two-domain-manager trial.
- Dashboard: `http://127.0.0.1:4174/console`, localhost only. HTML and actual
`/api/snapshot` returned HTTP 200 with installed project identity.
The protected fixture is intact. A reversible anchored `.git/info/exclude`
entry keeps it outside ownership and clean-checkout scheduling. No tracked
global ignore or user-file deletion was used. Initial `.torch` checkpoint:
`2a8ec35dac1c4b603297176fd1a8dcce36300d2b`.
## Real friction and its cost
1. PATH selected Codex 0.158.0 while this parent session used bundled 0.159.2.
The older child rejected `gpt-6.1-sol` with an account-support error. One
failed launch and a diagnostic replay preceded the correct executable retry.
This did not establish a subscription-wide lack of Sol access. Current CLI
0.159.2 subsequently ran the requested model successfully.
2. Startup errors omit Codex JSON stdout events. The displayed error was a
global Vercel MCP authentication failure, hiding the actual model rejection.
Manual capture of provider stdout was required. Unrelated MCP noise also
appeared in successful startup; it is not TORCH's communication server.
3. `torch brief` from both managed worktrees returned
`LOCAL_STATE_METADATA_MISMATCH`. Bound MCP queries/identity/ownership/inbox/
backlog worked, and the agents correctly reported the failure and stopped.
Fix must prove canonical-root/worktree membership rather than relaxing checks.
4. Generated `.torch` state requires a local commit before worktree creation;
provisioning then changes the tracked installation manifest. This requires
another checkpoint. Current docs don't guide that complete handoff clearly.
5. `doctor` is healthy despite an unavailable requested model or worktree-brief
failure. Installation integrity is not live startup/model qualification.
6. Manager should not receive specialist-only next-task instructions. In this
smoke policy the manager query was correctly refused; separate role-specific
startup steps would avoid an unnecessary error.
The plumbing works, but setup still requires engineering knowledge. It is not
yet an effortless end-user onboarding experience. The above incidents are in
`docs/TODO.md`, with distinct evidence and no claims of fixes not yet delivered.
## Provider-update improvement implemented during the first run
Owner explicitly requested current provider harnesses at install/startup.
New update preflight stages exact versions into TORCH-owned prefixes, verifies
version and parser startup, serializes updates, retains prior installs, and
selects absolute executable paths. Login/global settings are unchanged.
Automatic policy is owner-approved and per configured runtime, 24-hour freshness;
offline previews and failed refreshes do not launch stale providers silently.
Actual official-registry install/parser receipts:
- Codex `0.159.2`.
- Claude Code `2.1.285`.
- Pi `0.99.1` via current `@earendil-works/pi-coding-agent` package.
Receipt: `/tmp/torch-first-run-provider-update-result.json`. All three verified
`--version` and `--help`. Only Codex has live session qualification in this run.
Current Pi extension/flags and Claude lifecycle remain separately unqualified;
a newer binary doesn't establish adapter compatibility or model access.
Four new deterministic scenarios exercise official allowlist, exact executable,
fresh-cache/no-network behavior, fail-closed refresh, locking, tamper detection,
path safety and real CLI policy/startup boundaries. Existing self-host packaging
scenario now also excludes tracked project `.torch` state from engine candidates.
All 39 source acceptance groups PASS before checkpoint; installed candidate
qualification is recorded separately after build/activation.
## Pipeline addition: planned, not implemented
Spec section 14.0a now defines versioned optional project production flows,
deliverable instances linked to the existing backlog, pinned artifact/check/
approval lineage, safe rework, explicit waits, and a conditional Console view.
Frontend-design guidance shaped the row-to-detail progression, mobile blocker-
first presentation and evidence hierarchy without changing existing Console CSS.
There is no live pipeline runtime/view yet and no COMBATRIG pipeline alteration.
## Evidence receipts
- `/tmp/torch-real-candidate-build-20260930.json`
- `/tmp/torch-real-first-run-install.json`
- `/tmp/torch-first-run-model-list.json`
- `/tmp/torch-first-run-worktrees.json`
- `/tmp/torch-first-run-manager-start.json` (old CLI failure)
- `/tmp/torch-first-run-manager-diagnostic.json` (actual model error)
- `/tmp/torch-first-run-manager-current-cli-start.json` (Sol success)
- `/tmp/torch-first-run-luna-start.json` (Luna success)
- `/tmp/torch-first-run-alpha1-final-acceptance.json`
No cache savings, token cost or active-time metric is claimed: no attributable
runtime usage samples have been submitted. Pipeline metrics remain a design.
+4 -3
View File
@@ -4,13 +4,14 @@ import { createCodexAdapter } from './codex.mjs';
import { createPiAdapter } from './pi.mjs';
import { validateRuntimeAdapter } from './runtime.mjs';
import { inspectRuntimePluginTrust, loadTrustedRuntimeAdapters } from './plugins.mjs';
import { managedProvider } from './updates.mjs';
export class RuntimeAdapterRegistry {
constructor({ env = process.env, adapters = [], loadPlugins = false, pluginNames } = {}) {
this.adapters = new Map([
['claude', createClaudeAdapter({ env })],
['codex', createCodexAdapter({ env })],
['pi', createPiAdapter({ env })],
['claude', createClaudeAdapter({ env, executable: managedProvider('claude', { env })?.executable ?? 'claude' })],
['codex', createCodexAdapter({ env, executable: managedProvider('codex', { env })?.executable ?? 'codex' })],
['pi', createPiAdapter({ env, executable: managedProvider('pi', { env })?.executable ?? 'pi' })],
]);
this.pluginErrors = [];
this.trustedPlugins = [];
+147
View File
@@ -0,0 +1,147 @@
import { spawnSync } from 'node:child_process';
import { createHash, randomUUID } from 'node:crypto';
import { accessSync, constants, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, renameSync, rmdirSync, writeFileSync } from 'node:fs';
import { dirname, join, relative, resolve, sep } from 'node:path';
import { torchDataHome } from '../kernel/paths.mjs';
import { TorchError } from '../kernel/errors.mjs';
// Fixed official distribution names. Project input cannot supply npm packages,
// registries, scripts, prefixes or alternate update commands.
export const PROVIDER_PACKAGES = Object.freeze({
codex: '@openai/codex', claude: '@anthropic-ai/claude-code',
pi: '@earendil-works/pi-coding-agent',
});
const REGISTRY = 'https://registry.npmjs.org';
const VERSION = /^\d+\.\d+\.\d+$/;
function fail(message, details) {
return new TorchError(message, { code: 'RUNTIME_UPDATE_FAILED', details });
}
function safePath(path) {
let current = resolve(path);
for (;;) {
if (existsSync(current) && lstatSync(current).isSymbolicLink()) {
throw fail('Provider store must not traverse symlink directories', { path: current });
}
const parent = dirname(current);
if (parent === current) break;
current = parent;
}
}
export function providerStore(env = process.env) {
return join(torchDataHome(env), 'provider-runtimes');
}
function pointer(root, name) { return join(root, `${name}.json`); }
function hash(path) { return createHash('sha256').update(readFileSync(path)).digest('hex'); }
export function managedProvider(name, { env = process.env } = {}) {
if (!Object.hasOwn(PROVIDER_PACKAGES, name)) return null;
const root = providerStore(env);
safePath(root);
const path = pointer(root, name);
if (!existsSync(path)) return null;
safePath(path);
let record;
try { record = JSON.parse(readFileSync(path, 'utf8')); } catch {
throw fail('Provider activation record is unreadable', { name });
}
const prefix = resolve(record.prefix ?? '');
if (record.name !== name || record.package !== PROVIDER_PACKAGES[name]
|| !VERSION.test(record.version ?? '') || dirname(prefix) !== resolve(root)
|| !prefix.startsWith(join(root, `${name}-${record.version}-`))) {
throw fail('Provider activation record is invalid', { name });
}
safePath(prefix);
const executable = join(prefix, 'node_modules', '.bin', name);
try {
const target = realpathSync(executable);
if (relative(prefix, target).startsWith(`..${sep}`) || !target.startsWith(`${prefix}${sep}`)) throw Error('outside prefix');
accessSync(executable, constants.X_OK);
if (hash(target) !== record.sha256) throw Error('changed executable');
} catch {
throw fail('Managed provider executable is missing or changed', { name, executable });
}
return { ...record, executable };
}
export function planProviderUpdates(names, { env = process.env } = {}) {
return {
action: 'provider-update', scope: 'torch-managed-only', registry: REGISTRY,
providers: [...new Set(names)].map((name) => {
if (!Object.hasOwn(PROVIDER_PACKAGES, name)) {
throw fail('Automatic updates support only built-in providers', { name });
}
return { name, package: PROVIDER_PACKAGES[name], active: managedProvider(name, { env }) };
}),
root: providerStore(env), networkRequired: true, executesPackageInstallScripts: true,
modifiesGlobalInstallations: false, mutationPerformed: false,
};
}
export function updateProviders(names, { env = process.env, runner = spawnSync,
authorized = false, force = false, maxAgeHours = 24, now = () => new Date() } = {}) {
if (!authorized) throw new TorchError('Provider downloads require owner approval', { code: 'APPROVAL_REQUIRED' });
if (!Number.isInteger(maxAgeHours) || maxAgeHours < 1 || maxAgeHours > 168) {
throw fail('Provider freshness must be 1–168 hours');
}
const plan = planProviderUpdates(names, { env });
safePath(plan.root);
mkdirSync(plan.root, { recursive: true, mode: 0o700 });
const lock = join(plan.root, 'update.lock');
try { mkdirSync(lock); } catch {
throw new TorchError('Provider update already running; inspect update.lock before recovery', { code: 'RUNTIME_UPDATE_BUSY' });
}
const run = (command, args, cwd, timeout) => {
const result = runner(command, args, { cwd, env, encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'], timeout, maxBuffer: 8 * 1024 * 1024 });
if (result.status !== 0 || result.error || result.signal) {
throw fail('Provider update command failed; previous activation retained', {
command, args, status: result.status, signal: result.signal,
message: result.error?.message, stderr: String(result.stderr ?? '').slice(-2000),
});
}
return String(result.stdout ?? '').trim();
};
const results = [];
try {
for (const provider of plan.providers) {
const { name, package: pkg, active } = provider;
const age = active ? now().getTime() - Date.parse(active.checkedAt) : NaN;
if (!force && Number.isFinite(age) && age >= 0 && age < maxAgeHours * 3600000) {
results.push({ ...active, outcome: 'fresh' }); continue;
}
const version = JSON.parse(run('npm', ['view', `${pkg}@latest`, 'version', '--json', '--registry', REGISTRY], plan.root, 30000));
if (typeof version !== 'string' || !VERSION.test(version)) throw fail('Registry returned an invalid stable version', { name });
let record = active;
if (!active || active.version !== version) {
const prefix = mkdtempSync(join(plan.root, `${name}-${version}-`));
const args = ['install', '--prefix', prefix, '--no-audit', '--no-fund', '--save-exact', '--registry', REGISTRY];
if (name === 'pi') args.push('--ignore-scripts');
args.push(`${pkg}@${version}`);
run('npm', args, prefix, 180000);
const executable = join(prefix, 'node_modules', '.bin', name);
const target = realpathSync(executable);
if (!target.startsWith(`${prefix}${sep}`)) throw fail('Provider binary escaped its owned prefix', { name });
const actual = run(executable, ['--version'], prefix, 20000);
if (!actual.match(/\b\d+\.\d+\.\d+\b/) || actual.match(/\b\d+\.\d+\.\d+\b/)[0] !== version) {
throw fail('Installed provider version does not match the resolved release', { name, expected: version, actual });
}
// Smoke-check parser startup before activation. Real model/account and
// adapter-specific compatibility still require their own live gates.
run(executable, ['--help'], prefix, 20000);
record = { name, package: pkg, version, prefix, sha256: hash(target), previous: active?.prefix ?? null };
}
record = { ...record, checkedAt: now().toISOString() };
const temporary = join(plan.root, `${name}-${randomUUID()}.tmp`);
writeFileSync(temporary, `${JSON.stringify(record, null, 2)}\n`, { mode: 0o600, flag: 'wx' });
renameSync(temporary, pointer(plan.root, name));
results.push({ ...managedProvider(name, { env }), outcome: active?.version === version ? 'current' : 'updated' });
}
return { ...plan, results, mutationPerformed: true };
} finally {
rmdirSync(lock);
}
}
+67 -5
View File
@@ -3,6 +3,7 @@ import { BacklogService } from './backlog/service.mjs';
import { ArtifactService } from './artifacts/service.mjs';
import { createClaudeAdapter } from './adapters/claude.mjs';
import { createRuntimeAdapterRegistry } from './adapters/registry.mjs';
import { planProviderUpdates, updateProviders } from './adapters/updates.mjs';
import { profileCapabilityIssues, resolveLaunchPolicy } from './adapters/runtime.mjs';
import {
inspectRuntimePluginTrust, planRuntimePluginRevoke, planRuntimePluginTrust,
@@ -69,7 +70,7 @@ Usage:
torch architect validate --brief <path> --response <path> [--json]
torch architect run --brief <path> --provider <claude|codex> --output <path> [--model <model>] [--max-budget-usd <amount>] --yes [--json]
torch domains [--repo <path>] [--spec <path>] [--output <path>] [--json]
torch install --proposal <path> [--runtime <adapter,adapter>] [--default-runtime <adapter>] [--dry-run] [--yes] [--json]
torch install --proposal <path> [--runtime <adapter,adapter>] [--default-runtime <adapter>] [--update-runtimes] [--dry-run] [--yes] [--json]
torch worktrees [--parent <path>] [--dry-run] [--yes] [--json]
torch up [--fresh] [--only <id,id>] [--dry-run] [--yes] [--json]
torch down [--dry-run] [--yes] [--json]
@@ -91,6 +92,8 @@ Usage:
torch profile set --area <id> [--runtime <adapter>] [--model <model>|--inherit-model] [--reasoning <level|none>] [--launch-policy <field=value> ...] [--reset|--reset-launch-policy] [--dry-run|--yes] [--json]
torch profile defaults --runtime <adapter> [--model <model>] [--reasoning <level|none>] [--launch-policy <field=value> ...] [--reset|--reset-launch-policy] --yes [--json]
torch runtimes list [--json]
torch runtimes update --runtime <codex,claude,pi> [--force] [--dry-run|--yes] [--json]
torch runtimes update-policy --mode <auto|off> [--max-age-hours <1-168>] --yes [--json]
torch runtimes trust --name <id> --module </absolute/path/adapter.cjs> [--yes --sha256 <reviewed-hash>] [--json]
torch runtimes revoke --name <id> [--yes --sha256 <reviewed-hash>] [--json]
torch complete --area <id> --summary <text> [--task <id>] [--evidence <text>] [--commit <sha>] [--json]
@@ -415,6 +418,7 @@ export const CANDIDATE_ACCEPTANCE_EVIDENCE = Object.freeze({
'branches-worktrees': ['SCN-worktree-bootstrap'],
'runtime-identities': ['SCN-mixed-runtime'],
'runtime-profiles': ['SCN-runtime-profiles', 'SCN-runtime-cost-ceiling', 'SCN-runtime-profile-cli'],
'provider-updates': ['SCN-provider-updates', 'SCN-provider-updates-recovery', 'SCN-provider-update-safety', 'SCN-provider-update-cli'],
'console-runtime-profile': ['SCN-console-runtime-profile'],
'console-schedule-launcher': ['SCN-console-schedule-launcher'],
'runtime-adapters': [
@@ -552,6 +556,38 @@ export async function runCli(argv = process.argv.slice(2), {
}
if (command === 'runtimes') {
const operation = argv[1] ?? 'list';
if (operation === 'update') {
const names = commaList(optionValue(argv, '--runtime'));
if (!names.length) throw new TorchError('Select --runtime codex,claude,pi', { code: 'RUNTIME_UPDATE_INVALID' });
const plan = planProviderUpdates(names, { env });
if (argv.includes('--dry-run') || !argv.includes('--yes')) {
print({ ...plan, requiresApproval: true }, { json }); return 0;
}
print(updateProviders(names, { env, runner: spawn, authorized: true, force: argv.includes('--force') }), { json });
return 0;
}
if (operation === 'update-policy') {
const repository = inspectRepository(optionValue(argv, '--repo') ?? cwd);
const config = loadProjectConfig(repository.root);
const mode = optionValue(argv, '--mode');
const maxAge = Number(optionValue(argv, '--max-age-hours') ?? 24);
const policy = { mode, max_age_hours: maxAge };
const names = optionValue(argv, '--runtime') === undefined
? Object.keys(config.runtimes).filter(name => name !== 'default')
: commaList(optionValue(argv, '--runtime'));
planProviderUpdates(names, { env });
for (const name of names) {
if (!Object.hasOwn(config.runtimes, name)) throw new TorchError('Runtime is not configured', { code: 'RUNTIME_UPDATE_INVALID', details: { name } });
config.runtimes[name].updatePolicy = policy;
}
validateProjectConfig(config);
if (argv.includes('--dry-run') || !argv.includes('--yes')) {
print({ policy, runtimes: names, requiresApproval: true, mutationPerformed: false }, { json }); return 0;
}
print({ path: persistProfileConfig(repository.root, config), policy, runtimes: names,
sessionsStarted: false, mutationPerformed: true }, { json });
return 0;
}
if (operation === 'list') {
const builtIns = createRuntimeAdapterRegistry({ env, loadPlugins: false }).names();
let trust;
@@ -967,12 +1003,15 @@ export async function runCli(argv = process.argv.slice(2), {
const runtimes = runtimeOption === undefined ? undefined : commaList(runtimeOption);
if (argv.includes('--dry-run')) {
const proposal = loadProposal(cwd, proposalPath);
print({ ...planInstall({
const plan = planInstall({
repository, proposal, env, runtimes, defaultRuntime,
runtimeRegistry: createRuntimeAdapterRegistry({
env, loadPlugins: true, pluginNames: [...(runtimes ?? proposalRuntimeNames(proposal)), ...[defaultRuntime].filter(Boolean)],
}),
}), proposal: resolve(cwd, proposalPath) }, { json });
});
print({ ...plan, proposal: resolve(cwd, proposalPath),
...(argv.includes('--update-runtimes') ? { providerUpdates: planProviderUpdates(plan.runtimes, { env }) } : {}),
}, { json });
return 0;
}
if (!argv.includes('--yes')) {
@@ -981,12 +1020,25 @@ export async function runCli(argv = process.argv.slice(2), {
});
}
const proposal = loadProposal(cwd, proposalPath);
print(installProject({
if (argv.includes('--update-runtimes')) {
const installPlan = planInstall({ repository, proposal, env, runtimes, defaultRuntime });
if (!installPlan.canProceed) throw new TorchError('Install plan has blockers', { code: 'INSTALL_BLOCKED', details: installPlan.blockers });
updateProviders(installPlan.runtimes, { env, runner: spawn, authorized: true });
}
const installed = installProject({
repository, proposal, env, runtimes, defaultRuntime,
runtimeRegistry: createRuntimeAdapterRegistry({
env, loadPlugins: true, pluginNames: [...(runtimes ?? proposalRuntimeNames(proposal)), ...[defaultRuntime].filter(Boolean)],
}),
}), { json });
});
if (argv.includes('--update-runtimes')) {
const config = loadProjectConfig(repository.root);
for (const name of Object.keys(config.runtimes).filter(name => name !== 'default')) {
config.runtimes[name].updatePolicy = { mode: 'auto', max_age_hours: 24 };
}
persistProfileConfig(repository.root, config);
}
print(installed, { json });
return 0;
}
if (command === 'doctor') {
@@ -1010,6 +1062,16 @@ export async function runCli(argv = process.argv.slice(2), {
return 0;
}
if (command === 'up') {
const updateConfig = loadProjectConfig(repository.root);
if (argv.includes('--yes') && !argv.includes('--dry-run')) {
const selected = optionValue(argv, '--only') === undefined ? undefined : commaList(optionValue(argv, '--only'));
for (const name of configuredRuntimeNames(repository.root, selected)) {
const policy = updateConfig.runtimes[name]?.updatePolicy;
if (policy?.mode === 'auto') updateProviders([name], {
env, runner: spawn, authorized: true, maxAgeHours: policy.max_age_hours,
});
}
}
const control = openControlPlane({ repositoryRoot: repository.root, env });
try {
const only = optionValue(argv, '--only') === undefined ? undefined : commaList(optionValue(argv, '--only'));
+7
View File
@@ -320,6 +320,13 @@ export const projectConfigSchema = z.object({
}
}
for (const [runtime, profile] of Object.entries(config.runtimes)) {
if (runtime !== 'default' && profile?.updatePolicy !== undefined) {
const policy = z.object({ mode: z.enum(['auto', 'off']),
max_age_hours: z.number().int().min(1).max(168) }).strict().safeParse(profile.updatePolicy);
if (!policy.success) for (const issue of policy.error.issues) {
context.addIssue({ ...issue, path: ['runtimes', runtime, 'updatePolicy', ...issue.path] });
}
}
if (runtime === 'default' || !profile || typeof profile !== 'object' || Array.isArray(profile)) continue;
if (profile.launchPolicy !== undefined) {
const result = launchPolicy.safeParse(profile.launchPolicy);
+3 -2
View File
@@ -23,6 +23,7 @@ export const CANDIDATE_ACCEPTANCE_SCENARIOS = Object.freeze([
'branches-worktrees',
'runtime-identities',
'runtime-profiles',
'provider-updates',
'console-runtime-profile',
'console-schedule-launcher',
'runtime-adapters',
@@ -150,10 +151,10 @@ function gitSourceDescriptor(root) {
const excludedUntracked = execFileSync('git', ['-C', root, 'ls-files', '--others', '--exclude-standard', '-z'], {
encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
}).split('\0').filter(Boolean).sort();
const include = (name, entry) => name === 'node_modules'
const include = (name, entry) => name !== '.torch' && !name.startsWith('.torch/') && (name === 'node_modules'
|| name.startsWith('node_modules/')
|| tracked.has(name)
|| (entry.isDirectory() && [...tracked].some((path) => path.startsWith(`${name}/`)));
|| (entry.isDirectory() && [...tracked].some((path) => path.startsWith(`${name}/`))));
return { mode: 'git-commit', commit, include, excludedUntracked };
}
+138
View File
@@ -0,0 +1,138 @@
import assert from 'node:assert/strict';
import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, symlinkSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import test from 'node:test';
import { managedProvider, planProviderUpdates, providerStore, updateProviders } from '../../src/adapters/updates.mjs';
import { createRuntimeAdapterRegistry } from '../../src/adapters/registry.mjs';
import { execFileSync } from 'node:child_process';
import { inspectRepository } from '../../src/kernel/git.mjs';
import { analyzeRepository } from '../../src/kernel/analyze.mjs';
import { proposeDomains } from '../../src/kernel/domains.mjs';
import { installProject } from '../../src/kernel/install.mjs';
import { createWorktrees } from '../../src/kernel/worktrees.mjs';
import { runCli } from '../../src/cli.mjs';
function fixture() {
const root = mkdtempSync(join(tmpdir(), 'torch-provider-update-'));
const env = { ...process.env, XDG_DATA_HOME: root };
const calls = [];
let version = '1.2.3';
let failInstall = false;
const runner = (command, args, options) => {
calls.push({ command, args, options });
if (args[0] === 'view') return { status: 0, stdout: JSON.stringify(version) };
if (args[0] === 'install') {
if (failInstall) return { status: 1, stderr: 'offline' };
const pkg = args.at(-1);
const name = pkg.startsWith('@openai/') ? 'codex' : pkg.startsWith('@anthropic-ai/') ? 'claude' : 'pi';
const prefix = args[args.indexOf('--prefix') + 1];
mkdirSync(join(prefix, 'node_modules', '.bin'), { recursive: true });
const path = join(prefix, 'node_modules', '.bin', name);
writeFileSync(path, `#!/bin/sh\n# ${version}\n`);
chmodSync(path, 0o755);
return { status: 0, stdout: 'installed' };
}
return { status: 0, stdout: args[0] === '--version' ? `provider ${version}`
: args.includes('exec') ? '{"type":"thread.started","thread_id":"provider-update-fixture-session"}\n' : 'help' };
};
return { env, calls, runner, next: v => { version = v; }, breakInstall: () => { failInstall = true; } };
}
test('SCN-provider-updates: all built-ins resolve official latest, verify and launch the managed binary', () => {
const f = fixture();
const plan = planProviderUpdates(['codex', 'claude', 'pi'], f);
assert.equal(f.calls.length, 0);
assert.equal(plan.modifiesGlobalInstallations, false);
assert.throws(() => updateProviders(['codex'], f), { code: 'APPROVAL_REQUIRED' });
assert.equal(existsSync(providerStore(f.env)), false);
const result = updateProviders(['codex', 'claude', 'pi'], { ...f, authorized: true });
assert.deepEqual(result.results.map(r => r.outcome), ['updated', 'updated', 'updated']);
const registry = createRuntimeAdapterRegistry({ env: f.env });
for (const name of ['codex', 'claude', 'pi']) {
const selected = managedProvider(name, f);
assert.equal(registry.get(name).executable, selected.executable);
assert.equal(selected.version, '1.2.3');
assert.equal(f.calls.some(c => c.command === selected.executable && c.args[0] === '--version'), true);
assert.equal(f.calls.some(c => c.command === selected.executable && c.args[0] === '--help'), true);
}
for (const call of f.calls.filter(c => c.command === 'npm')) {
assert.equal(call.args.includes('-g'), false);
assert.equal(call.args.includes('--registry'), true);
assert.equal(call.options.timeout > 0, true);
}
assert.equal(f.calls.find(c => c.args.at(-1) === '@earendil-works/[email protected]').args.includes('--ignore-scripts'), true);
const count = f.calls.length;
assert.equal(updateProviders(['codex'], { ...f, authorized: true }).results[0].outcome, 'fresh');
assert.equal(f.calls.length, count, 'fresh startup performs no network/install calls');
});
test('SCN-provider-update-cli: opted-in startup updates selected provider, dry runs stay offline, failed refresh cannot launch', async () => {
const f = fixture();
const root = join(f.env.XDG_DATA_HOME, 'repo');
mkdirSync(root);
const git = args => execFileSync('git', ['-C', root, ...args], { stdio: 'pipe' });
git(['init', '-b', 'main']); git(['config', 'user.email', '[email protected]']); git(['config', 'user.name', 'Fixture']);
writeFileSync(join(root, 'README.md'), '# Test project\n');
git(['add', '.']); git(['commit', '-m', 'fixture']);
const repository = inspectRepository(root);
const proposal = proposeDomains({ repository, analysis: analyzeRepository(repository) });
proposal.review = { status: 'approved', reviewedAt: '2026-09-30T00:00:00Z', reviewedBy: 'fixture-owner', notes: [] };
installProject({ repository, proposal, env: f.env, runtimes: ['codex'] });
const stdout = process.stdout.write;
const outputs = [];
process.stdout.write = chunk => { outputs.push(String(chunk)); return true; };
try {
const run = args => runCli(args, { cwd: root, env: f.env, spawn: f.runner });
assert.equal(await run(['runtimes', 'update-policy', '--mode', 'auto', '--yes', '--json']), 0);
const config = JSON.parse(readFileSync(join(root, '.torch', 'torch.yaml')));
assert.deepEqual(config.runtimes.codex.updatePolicy, { mode: 'auto', max_age_hours: 24 });
git(['add', '.torch']); git(['commit', '-m', 'config']);
createWorktrees({ repository: inspectRepository(root), parentOverride: join(f.env.XDG_DATA_HOME, 'worktrees') });
assert.equal(await run(['up', '--fresh', '--only', 'session-manager', '--dry-run', '--json']), 0);
assert.equal(f.calls.length, 0, 'dry-run never downloads or launches');
assert.equal(await run(['up', '--fresh', '--only', 'session-manager', '--yes', '--json']), 0);
const selected = managedProvider('codex', f);
const launches = f.calls.filter(c => c.args.includes('exec'));
assert.equal(launches.length, 1);
assert.equal(launches[0].command, selected.executable);
assert.equal(f.calls.some(c => c.args.at(-1)?.startsWith('@anthropic-ai/')), false);
const pointer = join(providerStore(f.env), 'codex.json');
const stale = JSON.parse(readFileSync(pointer)); stale.checkedAt = '2000-01-01T00:00:00Z';
writeFileSync(pointer, JSON.stringify(stale)); f.next('1.3.0'); f.breakInstall();
assert.equal(await run(['up', '--only', 'session-manager', '--yes', '--json']), 2);
assert.equal(f.calls.filter(c => c.args.includes('exec')).length, 1, 'failed refresh must not launch the old provider silently');
assert.equal(outputs.some(s => s.includes('RUNTIME_UPDATE_FAILED')), true);
} finally { process.stdout.write = stdout; }
});
test('SCN-provider-updates-recovery: failed updates retain last activation; tampering and concurrent updates stop', () => {
const f = fixture();
updateProviders(['codex'], { ...f, authorized: true });
const active = managedProvider('codex', f);
f.next('1.3.0'); f.breakInstall();
assert.throws(() => updateProviders(['codex'], { ...f, authorized: true, force: true }), { code: 'RUNTIME_UPDATE_FAILED' });
assert.equal(managedProvider('codex', f).executable, active.executable);
assert.equal(managedProvider('codex', f).version, '1.2.3');
mkdirSync(join(providerStore(f.env), 'update.lock'));
assert.throws(() => updateProviders(['codex'], { ...f, authorized: true }), { code: 'RUNTIME_UPDATE_BUSY' });
writeFileSync(active.executable, 'tampered');
assert.throws(() => managedProvider('codex', f), { code: 'RUNTIME_UPDATE_FAILED' });
});
test('SCN-provider-update-safety: arbitrary packages, unsafe registry versions, path escape and symlink stores fail closed', () => {
const f = fixture();
assert.throws(() => planProviderUpdates(['untrusted-command'], f), { code: 'RUNTIME_UPDATE_FAILED' });
f.next('1.2.3;touch /tmp/evil');
assert.throws(() => updateProviders(['codex'], { ...f, authorized: true }), { code: 'RUNTIME_UPDATE_FAILED' });
assert.equal(f.calls.some(c => c.args[0] === 'install'), false);
f.next('1.2.3'); updateProviders(['codex'], { ...f, authorized: true });
const path = join(providerStore(f.env), 'codex.json');
const record = JSON.parse(readFileSync(path));
record.prefix = tmpdir(); writeFileSync(path, JSON.stringify(record));
assert.throws(() => managedProvider('codex', f), { code: 'RUNTIME_UPDATE_FAILED' });
const other = fixture();
mkdirSync(join(other.env.XDG_DATA_HOME, 'torch'));
symlinkSync(tmpdir(), providerStore(other.env));
assert.throws(() => planProviderUpdates(['pi'], other), { code: 'RUNTIME_UPDATE_FAILED' });
});
+4
View File
@@ -159,6 +159,8 @@ test('SCN-candidate-isolation: staging requires complete acceptance and never ch
writeFileSync(join(checkout, 'node_modules', 'fixture-dependency', 'index.js'), 'export const dependency = true;\n');
symlinkSync('../fixture-dependency/index.js', join(checkout, 'node_modules', '.bin', 'fixture-dependency'));
writeFileSync(join(checkout, '.gitignore'), 'node_modules/\nlocal-secret.env\n');
mkdirSync(join(checkout, '.torch'));
writeFileSync(join(checkout, '.torch', 'torch.yaml'), '{"machineSpecificProject":true}\n');
execFileSync('git', ['init', '-b', 'main', checkout]);
execFileSync('git', ['-C', checkout, 'config', 'user.email', '[email protected]']);
execFileSync('git', ['-C', checkout, 'config', 'user.name', 'TORCH Test']);
@@ -173,6 +175,8 @@ test('SCN-candidate-isolation: staging requires complete acceptance and never ch
assert.deepEqual(boundedPlan.excludedUntracked, ['new-runtime-adapter.mjs'],
'the reviewed candidate plan names untracked files that will not ship');
const bounded = service.installCandidate({ source: checkout, validate: accepted });
assert.equal(existsSync(join(bounded.destination, '.torch')), false,
'tracked self-host project state must not ship inside the installed engine');
assert.equal(existsSync(join(bounded.destination, 'local-secret.env')), false);
assert.equal(existsSync(join(bounded.destination, 'new-runtime-adapter.mjs')), false);
assert.equal(existsSync(join(bounded.destination, 'node_modules', '.bin', 'fixture-dependency')), true);
+1 -1
View File
@@ -1,6 +1,6 @@
{
"schema": "torch.dev/release/v1alpha1",
"version": "0.1.0-alpha.0",
"version": "0.1.0-alpha.1",
"state": {
"reads": ["torch.dev/state/v1alpha1"],
"writes": "torch.dev/state/v1alpha1",