@agfpd/iapeer 0.4.118 → 0.4.119
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/docs/20-opencode.md +13 -0
- package/package.json +1 -1
- package/src/cli/index.ts +6 -1
- package/src/identity/index.ts +17 -0
- package/src/init/index.ts +11 -7
- package/src/launch/invocation.ts +3 -1
- package/src/opencode/config.ts +5 -2
- package/src/storage/index.ts +26 -0
package/docs/20-opencode.md
CHANGED
|
@@ -139,6 +139,19 @@ there. Owner policy (2026-10-18): this is allowed, but never silent.
|
|
|
139
139
|
- The model actually used is reported by the plugin (`state.json` `model`) and
|
|
140
140
|
appears as the fleet snapshot field `runtimes[].model` (docs/15).
|
|
141
141
|
|
|
142
|
+
## Host-default provider
|
|
143
|
+
|
|
144
|
+
`<root>/runtimes/opencode/opencode.json` (operator-owned) is the host default:
|
|
145
|
+
every opencode peer starts from it, and the peer's own
|
|
146
|
+
`<private_dir>/opencode.json` overrides it key by key (deep merge; the inline
|
|
147
|
+
`OPENCODE_CONFIG_CONTENT` and fragments follow; provider pinning applies to the
|
|
148
|
+
result). Credentials stay references (`{env:NVIDIA_API_KEY}`); their values live
|
|
149
|
+
in the HOST launch.env `<root>/runtimes/opencode/launch.env`. Its env vars apply
|
|
150
|
+
to every peer's session of that runtime, under the peer's own launch.env (the
|
|
151
|
+
peer wins); launch directives (`PEER_START_ARGS`, `PEER_DISALLOWED_TOOLS`) stay
|
|
152
|
+
per-peer. With a host default in place, `add-runtime opencode --all` yields
|
|
153
|
+
working peers with no per-peer step.
|
|
154
|
+
|
|
142
155
|
## Durable inbound across a session death (OC07, all agent runtimes)
|
|
143
156
|
|
|
144
157
|
Invariant: an inbound the core acknowledged is either processed by a later
|
package/package.json
CHANGED
package/src/cli/index.ts
CHANGED
|
@@ -48,6 +48,7 @@ import {
|
|
|
48
48
|
killSession,
|
|
49
49
|
loadLifecycleConfig,
|
|
50
50
|
purgeIdentityState,
|
|
51
|
+
setLastDelivered,
|
|
51
52
|
removeSessionState,
|
|
52
53
|
resolvePeerRuntime,
|
|
53
54
|
setEphemeralArmed,
|
|
@@ -1378,7 +1379,7 @@ export async function sendMessage(
|
|
|
1378
1379
|
// unawaited in-process wake would die with it; the daemon's supervise-tick
|
|
1379
1380
|
// drain scan (≤60 s) picks the queue up — the EXISTING retry path for failed
|
|
1380
1381
|
// kicks, not a new mechanism.
|
|
1381
|
-
const { makeArmEphemeralOnDelivered, makeEphemeralRouteDeps, makeNoteLiveTopic } = await import('../daemon/main.ts')
|
|
1382
|
+
const { makeArmEphemeralOnDelivered, makeEphemeralRouteDeps, makeNoteInflight, makeNoteLiveTopic } = await import('../daemon/main.ts')
|
|
1382
1383
|
const cfg = loadLifecycleConfig(env)
|
|
1383
1384
|
const t0 = Date.now()
|
|
1384
1385
|
const result = await routeSend(
|
|
@@ -1397,6 +1398,10 @@ export async function sendMessage(
|
|
|
1397
1398
|
wake: cliWake,
|
|
1398
1399
|
ephemeral: makeEphemeralRouteDeps(cfg, env, () => {}),
|
|
1399
1400
|
noteLiveTopic: makeNoteLiveTopic(cfg, env),
|
|
1401
|
+
// CLI-path parity: the idle-proxy floor (В7) and the inflight ledger (OC07) — the daemon tick
|
|
1402
|
+
// redelivers a CLI-sent message too if its receiving session dies before answering.
|
|
1403
|
+
noteDelivered: identity => setLastDelivered(cfg, identity),
|
|
1404
|
+
noteInflight: makeNoteInflight(cfg),
|
|
1400
1405
|
originGuardBypass: opts.originGuardBypass,
|
|
1401
1406
|
},
|
|
1402
1407
|
)
|
package/src/identity/index.ts
CHANGED
|
@@ -677,6 +677,16 @@ export function ensurePeerProfile(options: EnsurePeerProfileOptions): PeerProfil
|
|
|
677
677
|
// preserved). resolvedIntelligence validates the explicit against the runtime's allowed set.
|
|
678
678
|
intelligence = resolvedIntelligence(options.runtime)
|
|
679
679
|
}
|
|
680
|
+
// OC01 — an EXPLICIT runtime the peer's nature does not allow is refused LOUDLY, exactly as on the
|
|
681
|
+
// new-profile branch (create). Before this, add-runtime <agentic> on a natural peer passed: the
|
|
682
|
+
// gate exempts the explicit runtime by design (an operator command is not discovery), so a human
|
|
683
|
+
// peer got an agent endpoint while its other agentic runtimes were dropped (linus 18.10).
|
|
684
|
+
if (!isIntelligenceAllowedForRuntime(options.runtime, intelligence)) {
|
|
685
|
+
throw new IapError(
|
|
686
|
+
`runtime "${options.runtime}" requires intelligence ∈ {${allowedIntelligencesForRuntime(options.runtime).join(', ')}}, ` +
|
|
687
|
+
`peer "${existing.personality}" is ${intelligence} — refusing to attach it`,
|
|
688
|
+
)
|
|
689
|
+
}
|
|
680
690
|
// Merge AFTER the intelligence is final, and GATE the WHOLE merged set (not just the
|
|
681
691
|
// fresh discoveries): for a natural/absent peer this both blocks a new agentic pickup
|
|
682
692
|
// AND heals a previously-leaked one (the arthur claude-leak class) on the next
|
|
@@ -688,6 +698,13 @@ export function ensurePeerProfile(options: EnsurePeerProfileOptions): PeerProfil
|
|
|
688
698
|
options.warn,
|
|
689
699
|
)
|
|
690
700
|
ensureLocalRuntimeScopes(cwd, mergedRuntimes)
|
|
701
|
+
const defaultRt = existing.runtime // the normalized default_runtime
|
|
702
|
+
if (defaultRt && !mergedRuntimes.includes(defaultRt)) {
|
|
703
|
+
options.warn?.(
|
|
704
|
+
`default_runtime "${defaultRt}" of "${existing.personality}" is not among its runtimes [${mergedRuntimes.join(', ')}] — ` +
|
|
705
|
+
`sends without a runtime will fail; set it with \`iapeer default-runtime <rt> --peer ${existing.personality}\``,
|
|
706
|
+
)
|
|
707
|
+
}
|
|
691
708
|
// В36 — an explicit description on RE-provision updates the local profile too (H1 in
|
|
692
709
|
// writePeerProfileAtomic still guards: absent/empty never wipes an existing value).
|
|
693
710
|
const explicitDescription = options.description?.trim()
|
package/src/init/index.ts
CHANGED
|
@@ -558,18 +558,22 @@ export async function initPeer(opts: InitPeerOptions): Promise<InitPeerResult> {
|
|
|
558
558
|
// exact file to fill, instead of letting the first wake be the first signal.
|
|
559
559
|
if (provisioned.runtime === 'opencode') {
|
|
560
560
|
const { opencodePaths } = await import('../opencode/config.ts')
|
|
561
|
+
const { opencodeHostConfigPath } = await import('../storage/index.ts')
|
|
561
562
|
const cfgPath = opencodePaths(provisioned.personality, { env }).config
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
563
|
+
const modelOf = (path: string): unknown => {
|
|
564
|
+
try {
|
|
565
|
+
return (JSON.parse(readFileSync(path, 'utf8')) as { model?: unknown }).model
|
|
566
|
+
} catch {
|
|
567
|
+
return undefined
|
|
568
|
+
}
|
|
567
569
|
}
|
|
570
|
+
// the peer's own config wins; else the host default (docs/20 §Host-default provider)
|
|
571
|
+
const model = modelOf(cfgPath) ?? modelOf(opencodeHostConfigPath({ env }))
|
|
568
572
|
if (typeof model !== 'string' || !model.includes('/')) {
|
|
569
573
|
opts.warn?.(
|
|
570
574
|
`opencode provider not configured for "${provisioned.personality}": it will run on OpenCode's own default ` +
|
|
571
|
-
`provider (doctrine and messages go there) until ${cfgPath}
|
|
572
|
-
|
|
575
|
+
`provider (doctrine and messages go there) until ${cfgPath} (per peer) or the host default ` +
|
|
576
|
+
`<root>/runtimes/opencode/opencode.json names "model": "<provider>/<model>" and that provider (docs/20)`,
|
|
573
577
|
)
|
|
574
578
|
}
|
|
575
579
|
}
|
package/src/launch/invocation.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// array straight to Bun.spawn — same argv, same env, different transport.
|
|
8
8
|
import { homedir } from 'os'
|
|
9
9
|
import { CODEX_BEARER_ENV_VAR, CODEX_DUMMY_BEARER } from '../core/constants.ts'
|
|
10
|
-
import { readLaunchEnv } from '../storage/index.ts'
|
|
10
|
+
import { readHostLaunchEnv, readLaunchEnv } from '../storage/index.ts'
|
|
11
11
|
import type { LaunchAdapterConfig, LaunchSpec, RuntimeAdapter } from './types.ts'
|
|
12
12
|
|
|
13
13
|
export interface LaunchInvocation {
|
|
@@ -46,6 +46,8 @@ export function buildLaunchInvocation(
|
|
|
46
46
|
const argv = adapter.buildArgv(specWithArgs, cfg)
|
|
47
47
|
const childEnv: NodeJS.ProcessEnv = {
|
|
48
48
|
...env,
|
|
49
|
+
// host-level launch.env of this runtime (e.g. a host-default provider credential), UNDER the peer's
|
|
50
|
+
...readHostLaunchEnv(spec.runtime, { env }),
|
|
49
51
|
...launchEnv.env,
|
|
50
52
|
// codex token-free MCP import: the bearer env var is set to the PUBLIC, non-secret stub so codex
|
|
51
53
|
// flips authStatus unsupported→bearer_token and imports send_to_peer (the OPEN daemon ignores it,
|
package/src/opencode/config.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { existsSync, mkdirSync, readFileSync } from 'node:fs'
|
|
|
4
4
|
import { join } from 'node:path'
|
|
5
5
|
import { pathToFileURL } from 'node:url'
|
|
6
6
|
import { isValidName } from '../core/constants.ts'
|
|
7
|
-
import { opencodePrivateDir, writeFileAtomic, type StorageOptions } from '../storage/index.ts'
|
|
7
|
+
import { opencodeHostConfigPath, opencodePrivateDir, writeFileAtomic, type StorageOptions } from '../storage/index.ts'
|
|
8
8
|
import pluginSource from './iapeer-plugin.mjs' with { type: 'text' }
|
|
9
9
|
import { listCapabilities } from '../capability/index.ts'
|
|
10
10
|
|
|
@@ -118,7 +118,10 @@ export const OPENCODE_GATED_PERMISSION = { bash: 'ask', edit: 'ask', webfetch: '
|
|
|
118
118
|
export function composeOpencodeConfig(input: ComposeOpencodeConfigInput, options: StorageOptions = {}): JsonObject {
|
|
119
119
|
if (!input.doctrine.trim()) throw new Error('OpenCode requires a non-empty composed system prompt')
|
|
120
120
|
const paths = opencodePaths(input.personality, options)
|
|
121
|
-
|
|
121
|
+
// Host default first (docs/20 §Host-default provider), then the peer's own config overrides it.
|
|
122
|
+
const hostPath = opencodeHostConfigPath(options)
|
|
123
|
+
let result: JsonObject = existsSync(hostPath) ? object(JSON.parse(readFileSync(hostPath, 'utf8')), hostPath) : {}
|
|
124
|
+
if (existsSync(paths.config)) result = mergeOpencodeConfig(result, object(JSON.parse(readFileSync(paths.config, 'utf8')), paths.config))
|
|
122
125
|
if (input.inline !== undefined) result = mergeOpencodeConfig(result, object(JSON.parse(input.inline), 'OPENCODE_CONFIG_CONTENT'))
|
|
123
126
|
// Fragment owners: the memory provider + every REGISTERED capability (docs/21) — never a
|
|
124
127
|
// hardcoded plugin list. An unregistered plugin's leftover fragment is not merged.
|
package/src/storage/index.ts
CHANGED
|
@@ -283,6 +283,32 @@ export function readLaunchEnv(cwd: string, runtime: Runtime): LaunchEnv {
|
|
|
283
283
|
} catch {
|
|
284
284
|
return { startArgs: [], env: {} }
|
|
285
285
|
}
|
|
286
|
+
return parseLaunchEnv(text)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** HOST-level launch.env of a runtime: `<root>/runtimes/<rt>/launch.env` (same format). Its env vars
|
|
290
|
+
* apply to EVERY peer's session of that runtime, under the peer's own launch.env (the peer wins) — the
|
|
291
|
+
* place for a host-default provider credential (docs/20 §Host-default provider). Directives
|
|
292
|
+
* (PEER_START_ARGS / PEER_DISALLOWED_TOOLS) stay per-peer only. */
|
|
293
|
+
export function hostLaunchEnvPath(runtime: Runtime, options: StorageOptions = {}): string {
|
|
294
|
+
return join(options.rootDir ?? resolveGlobalRoot(options.env), RUNTIMES_DIR, runtime, LAUNCH_ENV_FILE)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export function readHostLaunchEnv(runtime: Runtime, options: StorageOptions = {}): Record<string, string> {
|
|
298
|
+
try {
|
|
299
|
+
return parseLaunchEnv(readFileSync(hostLaunchEnvPath(runtime, options), 'utf8')).env
|
|
300
|
+
} catch {
|
|
301
|
+
return {}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** The host-default OpenCode configuration (operator-owned): `<root>/runtimes/opencode/opencode.json`.
|
|
306
|
+
* Every opencode peer starts from it; the peer's own `<private_dir>/opencode.json` overrides it. */
|
|
307
|
+
export function opencodeHostConfigPath(options: StorageOptions = {}): string {
|
|
308
|
+
return join(options.rootDir ?? resolveGlobalRoot(options.env), RUNTIMES_DIR, 'opencode', 'opencode.json')
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function parseLaunchEnv(text: string): LaunchEnv {
|
|
286
312
|
const env: Record<string, string> = {}
|
|
287
313
|
for (const rawLine of text.split(/\r?\n/)) {
|
|
288
314
|
const line = rawLine.trim()
|