@agfpd/iapeer 0.2.12 → 0.2.13
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/package.json +1 -1
- package/src/cli/index.ts +32 -0
- package/src/index.ts +2 -0
- package/src/launch/nativeMemory.test.ts +150 -0
- package/src/launch/nativeMemory.ts +185 -0
- package/src/provision/index.ts +34 -0
- package/src/provision/provision.test.ts +41 -0
package/package.json
CHANGED
package/src/cli/index.ts
CHANGED
|
@@ -368,6 +368,7 @@ const USAGE = `usage: iapeer <verb> [args]
|
|
|
368
368
|
interrupt <peer> [runtime] interrupt the current turn (Escape) — context intact
|
|
369
369
|
compact <peer> [runtime] compact the peer's context (/compact)
|
|
370
370
|
self-fresh (agent self-call) mark /new eager-fresh + self-kill — the daemon relaunches fresh
|
|
371
|
+
native-memory <off|on> (--peer <p> | --all) gate/restore runtimes' native memory (canonized lever; контракт «Слот памяти»)
|
|
371
372
|
`
|
|
372
373
|
|
|
373
374
|
export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.env): Promise<number> {
|
|
@@ -422,6 +423,37 @@ export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.en
|
|
|
422
423
|
out(`memory: ${mem.state}${mem.detail ? ` — ${mem.detail}` : ''} (slot: ${memLabel})\n`)
|
|
423
424
|
return r.marketplaces.some(m => m.state === 'failed') || infraFailed ? 1 : 0
|
|
424
425
|
}
|
|
426
|
+
case 'native-memory': {
|
|
427
|
+
// The canonized runtime-memory lever (контракт «Слот памяти» §Native-память):
|
|
428
|
+
// off = explicit disable merged into the peer's runtime config files; on =
|
|
429
|
+
// remove the key (restore the runtime's own default). Consumers: the operator
|
|
430
|
+
// and the memory provider's install-time sweep (`--all`). NOT slot-gated here —
|
|
431
|
+
// an explicit operator/provider action; only the BIRTH-time hook is slot-gated.
|
|
432
|
+
const state = positionals[0]
|
|
433
|
+
if (state !== 'off' && state !== 'on') return usage(errOut)
|
|
434
|
+
const peerName = typeof flags.peer === 'string' ? flags.peer : undefined
|
|
435
|
+
if (flags.all !== true && !peerName) return usage(errOut)
|
|
436
|
+
const { applyNativeMemory } = await import('../launch/nativeMemory.ts')
|
|
437
|
+
const index = readPeersIndex({ env })
|
|
438
|
+
const targets = flags.all === true ? index.peers : index.peers.filter(p => p.personality === peerName)
|
|
439
|
+
if (targets.length === 0) {
|
|
440
|
+
errOut(`peer "${peerName ?? ''}" is not in the iapeer peers index\n`)
|
|
441
|
+
return 1
|
|
442
|
+
}
|
|
443
|
+
let failed = false
|
|
444
|
+
for (const p of targets) {
|
|
445
|
+
const outcomes = applyNativeMemory(p.cwd, p.runtimes, state)
|
|
446
|
+
if (outcomes.length === 0) {
|
|
447
|
+
out(`${p.personality}: no claude/codex runtime — skipped\n`)
|
|
448
|
+
continue
|
|
449
|
+
}
|
|
450
|
+
for (const o of outcomes) {
|
|
451
|
+
out(`${p.personality} (${o.runtime}): ${o.state}${o.detail ? ` — ${o.detail}` : ''}\n`)
|
|
452
|
+
if (o.state === 'failed') failed = true
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
return failed ? 1 : 0
|
|
456
|
+
}
|
|
425
457
|
case 'status': {
|
|
426
458
|
// Host snapshot (контракт «Слот памяти» §status): version + daemon health +
|
|
427
459
|
// the memory-slot line. Exit 1 iff the daemon is unhealthy (usable as a
|
package/src/index.ts
CHANGED
|
@@ -28,5 +28,7 @@ export * from './onboard/index.ts'
|
|
|
28
28
|
// Memory slot — the declarative provider slot (контракт «Слот памяти»): status read + onboard step.
|
|
29
29
|
export * from './status/index.ts'
|
|
30
30
|
export * from './onboard/memory.ts'
|
|
31
|
+
// Native runtime-memory levers (slot contract §Native-память): canonized forms + verb mechanics.
|
|
32
|
+
export * from './launch/nativeMemory.ts'
|
|
31
33
|
export { composeSystemPrompt, gatherPromptInput } from './launch/composeSystemPrompt.ts'
|
|
32
34
|
export type { GatherPromptOptions } from './launch/composeSystemPrompt.ts'
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// Native runtime-memory levers — the canonized forms (slot contract §Native-
|
|
2
|
+
// память). The HARD requirement pinned here: NO-CLOBBER — both target files
|
|
3
|
+
// carry foreign blocks (plugin enables, statusline wrappers) that every lever
|
|
4
|
+
// write must preserve byte-meaningfully.
|
|
5
|
+
|
|
6
|
+
import { afterEach, describe, expect, test } from 'bun:test'
|
|
7
|
+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'fs'
|
|
8
|
+
import { tmpdir } from 'os'
|
|
9
|
+
import { join } from 'path'
|
|
10
|
+
import {
|
|
11
|
+
applyNativeMemory,
|
|
12
|
+
claudeSettingsPath,
|
|
13
|
+
codexProjectConfigPath,
|
|
14
|
+
codexGlobalConfigPath,
|
|
15
|
+
preTrustCodexCwd,
|
|
16
|
+
} from './nativeMemory.ts'
|
|
17
|
+
|
|
18
|
+
const dirs: string[] = []
|
|
19
|
+
function mkTmp(): string {
|
|
20
|
+
const d = mkdtempSync(join(tmpdir(), 'iapeer-natmem-'))
|
|
21
|
+
dirs.push(d)
|
|
22
|
+
return d
|
|
23
|
+
}
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
while (dirs.length) rmSync(dirs.pop()!, { recursive: true, force: true })
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
describe('claude lever (settings.json merge)', () => {
|
|
29
|
+
test('absent file → created with ONLY the lever key', () => {
|
|
30
|
+
const cwd = mkTmp()
|
|
31
|
+
const [o] = applyNativeMemory(cwd, ['claude'], 'off')
|
|
32
|
+
expect(o?.state).toBe('written')
|
|
33
|
+
expect(JSON.parse(readFileSync(claudeSettingsPath(cwd), 'utf8'))).toEqual({ autoMemoryEnabled: false })
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test('NO-CLOBBER: foreign keys (plugins, statusline) survive off AND on', () => {
|
|
37
|
+
const cwd = mkTmp()
|
|
38
|
+
mkdirSync(join(cwd, '.claude'), { recursive: true })
|
|
39
|
+
const foreign = { enabledPlugins: { 'MergeMind@agfpd': true }, statusLine: { type: 'command', command: 'x' } }
|
|
40
|
+
writeFileSync(claudeSettingsPath(cwd), JSON.stringify(foreign))
|
|
41
|
+
applyNativeMemory(cwd, ['claude'], 'off')
|
|
42
|
+
const afterOff = JSON.parse(readFileSync(claudeSettingsPath(cwd), 'utf8'))
|
|
43
|
+
expect(afterOff).toEqual({ ...foreign, autoMemoryEnabled: false })
|
|
44
|
+
const [on] = applyNativeMemory(cwd, ['claude'], 'on')
|
|
45
|
+
expect(on?.state).toBe('written')
|
|
46
|
+
expect(JSON.parse(readFileSync(claudeSettingsPath(cwd), 'utf8'))).toEqual(foreign) // key REMOVED, default restored
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test('idempotent: already-false → already; on with no key → already', () => {
|
|
50
|
+
const cwd = mkTmp()
|
|
51
|
+
applyNativeMemory(cwd, ['claude'], 'off')
|
|
52
|
+
expect(applyNativeMemory(cwd, ['claude'], 'off')[0]?.state).toBe('already')
|
|
53
|
+
applyNativeMemory(cwd, ['claude'], 'on')
|
|
54
|
+
expect(applyNativeMemory(cwd, ['claude'], 'on')[0]?.state).toBe('already')
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('non-object settings.json → failed (refuses to clobber), file untouched', () => {
|
|
58
|
+
const cwd = mkTmp()
|
|
59
|
+
mkdirSync(join(cwd, '.claude'), { recursive: true })
|
|
60
|
+
writeFileSync(claudeSettingsPath(cwd), '"just a string"')
|
|
61
|
+
const [o] = applyNativeMemory(cwd, ['claude'], 'off')
|
|
62
|
+
expect(o?.state).toBe('failed')
|
|
63
|
+
expect(readFileSync(claudeSettingsPath(cwd), 'utf8')).toBe('"just a string"')
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
describe('codex lever (config.toml [features] merge)', () => {
|
|
68
|
+
test('absent file → [features] section with the lever line', () => {
|
|
69
|
+
const cwd = mkTmp()
|
|
70
|
+
const [o] = applyNativeMemory(cwd, ['codex'], 'off')
|
|
71
|
+
expect(o?.state).toBe('written')
|
|
72
|
+
expect(readFileSync(codexProjectConfigPath(cwd), 'utf8')).toBe('[features]\nmemories = false\n')
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test('NO-CLOBBER: plugin blocks survive; [features] appended after them (fleet-file shape)', () => {
|
|
76
|
+
const cwd = mkTmp()
|
|
77
|
+
mkdirSync(join(cwd, '.codex'), { recursive: true })
|
|
78
|
+
const fleet = '[plugins."Inter-Agent-Protocol@agfpd"]\nenabled = true\n\n[plugins."MergeMind@agfpd"]\nenabled = true\n'
|
|
79
|
+
writeFileSync(codexProjectConfigPath(cwd), fleet)
|
|
80
|
+
applyNativeMemory(cwd, ['codex'], 'off')
|
|
81
|
+
const text = readFileSync(codexProjectConfigPath(cwd), 'utf8')
|
|
82
|
+
expect(text).toContain('[plugins."Inter-Agent-Protocol@agfpd"]\nenabled = true')
|
|
83
|
+
expect(text).toContain('[plugins."MergeMind@agfpd"]\nenabled = true')
|
|
84
|
+
expect(text).toContain('[features]\nmemories = false')
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
test('existing [features] WITHOUT memories (followed by another section) → inserted INSIDE the section', () => {
|
|
88
|
+
const cwd = mkTmp()
|
|
89
|
+
mkdirSync(join(cwd, '.codex'), { recursive: true })
|
|
90
|
+
writeFileSync(codexProjectConfigPath(cwd), '[features]\nother = true\n\n[plugins."X@y"]\nenabled = true\n')
|
|
91
|
+
applyNativeMemory(cwd, ['codex'], 'off')
|
|
92
|
+
const text = readFileSync(codexProjectConfigPath(cwd), 'utf8')
|
|
93
|
+
// the lever line lands in [features], NOT in [plugins."X@y"]
|
|
94
|
+
expect(text.indexOf('memories = false')).toBeLessThan(text.indexOf('[plugins."X@y"]'))
|
|
95
|
+
expect(text).toContain('other = true')
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
test('memories = true → replaced with false; already false → already', () => {
|
|
99
|
+
const cwd = mkTmp()
|
|
100
|
+
mkdirSync(join(cwd, '.codex'), { recursive: true })
|
|
101
|
+
writeFileSync(codexProjectConfigPath(cwd), '[features]\nmemories = true\n')
|
|
102
|
+
expect(applyNativeMemory(cwd, ['codex'], 'off')[0]?.state).toBe('written')
|
|
103
|
+
expect(readFileSync(codexProjectConfigPath(cwd), 'utf8')).toContain('memories = false')
|
|
104
|
+
expect(applyNativeMemory(cwd, ['codex'], 'off')[0]?.state).toBe('already')
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
test('on → the memories line is REMOVED (default restored), section + foreign lines kept', () => {
|
|
108
|
+
const cwd = mkTmp()
|
|
109
|
+
mkdirSync(join(cwd, '.codex'), { recursive: true })
|
|
110
|
+
writeFileSync(codexProjectConfigPath(cwd), '[features]\nmemories = false\nother = true\n')
|
|
111
|
+
const [o] = applyNativeMemory(cwd, ['codex'], 'on')
|
|
112
|
+
expect(o?.state).toBe('written')
|
|
113
|
+
const text = readFileSync(codexProjectConfigPath(cwd), 'utf8')
|
|
114
|
+
expect(text).not.toContain('memories')
|
|
115
|
+
expect(text).toContain('other = true')
|
|
116
|
+
expect(applyNativeMemory(cwd, ['codex'], 'on')[0]?.state).toBe('already')
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
describe('applyNativeMemory runtime dispatch', () => {
|
|
121
|
+
test('both runtimes → both levers; router-only runtimes → no outcomes', () => {
|
|
122
|
+
const cwd = mkTmp()
|
|
123
|
+
const both = applyNativeMemory(cwd, ['claude', 'codex'], 'off')
|
|
124
|
+
expect(both.map(o => o.runtime).sort()).toEqual(['claude', 'codex'])
|
|
125
|
+
expect(applyNativeMemory(mkTmp(), ['telegram', 'notifier'], 'off')).toEqual([])
|
|
126
|
+
})
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
describe('preTrustCodexCwd (birth-time trust, codex global config)', () => {
|
|
130
|
+
test('appends a [projects] block once; NO-CLOBBER of existing content; idempotent', () => {
|
|
131
|
+
const home = mkTmp()
|
|
132
|
+
const env = { HOME: home } as NodeJS.ProcessEnv
|
|
133
|
+
mkdirSync(join(home, '.codex'), { recursive: true })
|
|
134
|
+
const existing = '[projects."/already/trusted"]\ntrust_level = "trusted"\n'
|
|
135
|
+
writeFileSync(codexGlobalConfigPath(env), existing)
|
|
136
|
+
const first = preTrustCodexCwd('/peers/newborn', env)
|
|
137
|
+
expect(first.state).toBe('written')
|
|
138
|
+
const text = readFileSync(codexGlobalConfigPath(env), 'utf8')
|
|
139
|
+
expect(text).toContain(existing.trim())
|
|
140
|
+
expect(text).toContain('[projects."/peers/newborn"]\ntrust_level = "trusted"')
|
|
141
|
+
expect(preTrustCodexCwd('/peers/newborn', env).state).toBe('already')
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
test('absent global config → created with just the block', () => {
|
|
145
|
+
const home = mkTmp()
|
|
146
|
+
const env = { HOME: home } as NodeJS.ProcessEnv
|
|
147
|
+
expect(preTrustCodexCwd('/peers/first', env).state).toBe('written')
|
|
148
|
+
expect(readFileSync(codexGlobalConfigPath(env), 'utf8')).toBe('[projects."/peers/first"]\ntrust_level = "trusted"\n')
|
|
149
|
+
})
|
|
150
|
+
})
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// Native runtime-memory levers (контракт «Слот памяти» §Native-память рантаймов
|
|
2
|
+
// при занятом слоте, agreed 10.06). The CORE canonizes the lever FORMS — runtime
|
|
3
|
+
// knowledge lives in the runtime layer, like deliveryMarkers/buildArgv:
|
|
4
|
+
//
|
|
5
|
+
// claude: merge `"autoMemoryEnabled": false` into `<cwd>/.claude/settings.json`
|
|
6
|
+
// codex: merge `memories = false` into the `[features]` section of
|
|
7
|
+
// `<cwd>/.codex/config.toml`
|
|
8
|
+
//
|
|
9
|
+
// Both files CARRY FOREIGN BLOCKS (plugin enables, statusline wrappers, …) —
|
|
10
|
+
// every write here is a NO-CLOBBER merge (parse/patch + atomic temp+rename),
|
|
11
|
+
// never a rewrite. Forms verified live on the fleet (12/12 + behavioral smoke
|
|
12
|
+
// 09.06; codex reads project-local config when the cwd is TRUSTED — see
|
|
13
|
+
// preTrustCodexCwd).
|
|
14
|
+
//
|
|
15
|
+
// `off` writes the explicit disable; `on` REMOVES the key (restores the
|
|
16
|
+
// runtime's own default rather than baking an explicit enable — the core has
|
|
17
|
+
// no opinion beyond "parallel store off while a memory provider owns the host").
|
|
18
|
+
//
|
|
19
|
+
// Consumers: the operator verb `iapeer native-memory <off|on>`, the provider's
|
|
20
|
+
// install-time sweep (calls the verb), and provisionPeer's birth-time hook
|
|
21
|
+
// (slot-gated, see provision/index.ts).
|
|
22
|
+
|
|
23
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync } from 'fs'
|
|
24
|
+
import { dirname, join } from 'path'
|
|
25
|
+
import { homedir } from 'os'
|
|
26
|
+
import type { Runtime } from '../core/constants.ts'
|
|
27
|
+
import { writeFileAtomic } from '../storage/index.ts'
|
|
28
|
+
|
|
29
|
+
export type NativeMemoryState = 'off' | 'on'
|
|
30
|
+
|
|
31
|
+
export interface LeverOutcome {
|
|
32
|
+
runtime: 'claude' | 'codex'
|
|
33
|
+
path: string
|
|
34
|
+
state: 'written' | 'already' | 'failed'
|
|
35
|
+
detail?: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ─── claude: <cwd>/.claude/settings.json — JSON merge ───────────────────────
|
|
39
|
+
|
|
40
|
+
export function claudeSettingsPath(cwd: string): string {
|
|
41
|
+
return join(cwd, '.claude', 'settings.json')
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function applyClaudeLever(cwd: string, state: NativeMemoryState): LeverOutcome {
|
|
45
|
+
const path = claudeSettingsPath(cwd)
|
|
46
|
+
try {
|
|
47
|
+
let obj: Record<string, unknown> = {}
|
|
48
|
+
if (existsSync(path)) {
|
|
49
|
+
const raw = JSON.parse(readFileSync(path, 'utf8')) as unknown
|
|
50
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
51
|
+
return { runtime: 'claude', path, state: 'failed', detail: 'settings.json is not a JSON object — refusing to clobber' }
|
|
52
|
+
}
|
|
53
|
+
obj = raw as Record<string, unknown>
|
|
54
|
+
}
|
|
55
|
+
if (state === 'off') {
|
|
56
|
+
if (obj.autoMemoryEnabled === false) return { runtime: 'claude', path, state: 'already' }
|
|
57
|
+
obj.autoMemoryEnabled = false
|
|
58
|
+
} else {
|
|
59
|
+
if (!('autoMemoryEnabled' in obj)) return { runtime: 'claude', path, state: 'already' }
|
|
60
|
+
delete obj.autoMemoryEnabled // restore the runtime's own default
|
|
61
|
+
}
|
|
62
|
+
mkdirSync(dirname(path), { recursive: true })
|
|
63
|
+
writeFileAtomic(path, `${JSON.stringify(obj, null, 2)}\n`)
|
|
64
|
+
return { runtime: 'claude', path, state: 'written' }
|
|
65
|
+
} catch (e) {
|
|
66
|
+
return { runtime: 'claude', path, state: 'failed', detail: e instanceof Error ? e.message : String(e) }
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ─── codex: <cwd>/.codex/config.toml — section-scoped line merge ─────────────
|
|
71
|
+
|
|
72
|
+
export function codexProjectConfigPath(cwd: string): string {
|
|
73
|
+
return join(cwd, '.codex', 'config.toml')
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Locate the `[features]` section bounds: [startLine, endLine) of its BODY
|
|
77
|
+
* (after the header, up to the next `[…]` header or EOF). null = no section. */
|
|
78
|
+
function featuresBounds(lines: string[]): { header: number; start: number; end: number } | null {
|
|
79
|
+
const header = lines.findIndex(l => l.trim() === '[features]')
|
|
80
|
+
if (header < 0) return null
|
|
81
|
+
let end = lines.length
|
|
82
|
+
for (let i = header + 1; i < lines.length; i++) {
|
|
83
|
+
if (/^\s*\[/.test(lines[i]!)) {
|
|
84
|
+
end = i
|
|
85
|
+
break
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return { header, start: header + 1, end }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function applyCodexLever(cwd: string, state: NativeMemoryState): LeverOutcome {
|
|
92
|
+
const path = codexProjectConfigPath(cwd)
|
|
93
|
+
try {
|
|
94
|
+
const text = existsSync(path) ? readFileSync(path, 'utf8') : ''
|
|
95
|
+
const lines = text.length ? text.split('\n') : []
|
|
96
|
+
const bounds = featuresBounds(lines)
|
|
97
|
+
const memRe = /^\s*memories\s*=/
|
|
98
|
+
if (state === 'off') {
|
|
99
|
+
if (bounds) {
|
|
100
|
+
const idx = lines.slice(bounds.start, bounds.end).findIndex(l => memRe.test(l))
|
|
101
|
+
if (idx >= 0) {
|
|
102
|
+
const abs = bounds.start + idx
|
|
103
|
+
if (/^\s*memories\s*=\s*false\s*$/.test(lines[abs]!)) return { runtime: 'codex', path, state: 'already' }
|
|
104
|
+
lines[abs] = 'memories = false'
|
|
105
|
+
} else {
|
|
106
|
+
lines.splice(bounds.start, 0, 'memories = false')
|
|
107
|
+
}
|
|
108
|
+
} else {
|
|
109
|
+
if (lines.length && lines[lines.length - 1]!.trim() !== '') lines.push('')
|
|
110
|
+
lines.push('[features]', 'memories = false')
|
|
111
|
+
}
|
|
112
|
+
} else {
|
|
113
|
+
if (!bounds) return { runtime: 'codex', path, state: 'already' }
|
|
114
|
+
const idx = lines.slice(bounds.start, bounds.end).findIndex(l => memRe.test(l))
|
|
115
|
+
if (idx < 0) return { runtime: 'codex', path, state: 'already' }
|
|
116
|
+
lines.splice(bounds.start + idx, 1) // restore the runtime's own default
|
|
117
|
+
}
|
|
118
|
+
mkdirSync(dirname(path), { recursive: true })
|
|
119
|
+
const outText = lines.join('\n')
|
|
120
|
+
writeFileAtomic(path, outText.endsWith('\n') ? outText : `${outText}\n`)
|
|
121
|
+
return { runtime: 'codex', path, state: 'written' }
|
|
122
|
+
} catch (e) {
|
|
123
|
+
return { runtime: 'codex', path, state: 'failed', detail: e instanceof Error ? e.message : String(e) }
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ─── codex trust (birth-time only) ───────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
/** The codex GLOBAL config (`~/.codex/config.toml`) — where trust lives as
|
|
130
|
+
* `[projects."<path>"] trust_level = "trusted"` blocks. */
|
|
131
|
+
export function codexGlobalConfigPath(env: NodeJS.ProcessEnv = process.env): string {
|
|
132
|
+
const home = env.HOME?.trim() || homedir()
|
|
133
|
+
return join(home, '.codex', 'config.toml')
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Pre-trust a NEWBORN codex peer's cwd by appending its `[projects."<cwd>"]`
|
|
138
|
+
* block to the codex global config (no-clobber: only when no block for this
|
|
139
|
+
* path exists). Closes the contract requirement «рычаг действует с ПЕРВОЙ
|
|
140
|
+
* сессии» DETERMINISTICALLY: codex reads project-local config only for a
|
|
141
|
+
* trusted cwd, and without pre-trust the first session's trust is granted
|
|
142
|
+
* mid-boot by the adapter's dialog auto-accept — whether the project config is
|
|
143
|
+
* (re)read after that grant within the same run is codex-internal. Birth-time
|
|
144
|
+
* only — existing peers are already trusted through their boot history.
|
|
145
|
+
*/
|
|
146
|
+
export function preTrustCodexCwd(cwd: string, env: NodeJS.ProcessEnv = process.env): LeverOutcome {
|
|
147
|
+
const path = codexGlobalConfigPath(env)
|
|
148
|
+
try {
|
|
149
|
+
// Codex keys trust on the RESOLVED real path (live fact, 10.06 acceptance: the
|
|
150
|
+
// /tmp → /private/tmp symlink made a literal-path entry MISS and left the lever
|
|
151
|
+
// inert — trust entries written by codex itself are all /private/tmp/...).
|
|
152
|
+
let real = cwd
|
|
153
|
+
try {
|
|
154
|
+
real = realpathSync(cwd)
|
|
155
|
+
} catch {
|
|
156
|
+
/* cwd not on disk yet → keep as given */
|
|
157
|
+
}
|
|
158
|
+
const text = existsSync(path) ? readFileSync(path, 'utf8') : ''
|
|
159
|
+
if (text.includes(`[projects."${real}"]`)) return { runtime: 'codex', path, state: 'already' }
|
|
160
|
+
const block = `${text.length && !text.endsWith('\n') ? '\n' : ''}${text.trim().length ? '\n' : ''}[projects."${real}"]\ntrust_level = "trusted"\n`
|
|
161
|
+
mkdirSync(dirname(path), { recursive: true })
|
|
162
|
+
writeFileAtomic(path, text + block)
|
|
163
|
+
return { runtime: 'codex', path, state: 'written' }
|
|
164
|
+
} catch (e) {
|
|
165
|
+
return { runtime: 'codex', path, state: 'failed', detail: e instanceof Error ? e.message : String(e) }
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ─── public entry ────────────────────────────────────────────────────────────
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Apply the native-memory lever for one peer cwd across its declared runtimes
|
|
173
|
+
* (∩ {claude, codex} — other runtimes carry no native memory). Idempotent;
|
|
174
|
+
* every outcome is reported (a 'failed' lever never throws).
|
|
175
|
+
*/
|
|
176
|
+
export function applyNativeMemory(
|
|
177
|
+
cwd: string,
|
|
178
|
+
runtimes: readonly Runtime[],
|
|
179
|
+
state: NativeMemoryState,
|
|
180
|
+
): LeverOutcome[] {
|
|
181
|
+
const out: LeverOutcome[] = []
|
|
182
|
+
if (runtimes.includes('claude')) out.push(applyClaudeLever(cwd, state))
|
|
183
|
+
if (runtimes.includes('codex')) out.push(applyCodexLever(cwd, state))
|
|
184
|
+
return out
|
|
185
|
+
}
|
package/src/provision/index.ts
CHANGED
|
@@ -91,6 +91,40 @@ export async function provisionPeer(opts: ProvisionPeerOptions): Promise<Provisi
|
|
|
91
91
|
runtimeBin,
|
|
92
92
|
warn: opts.warn,
|
|
93
93
|
})
|
|
94
|
+
// Birth-time native-memory lever (контракт «Слот памяти» §Native-память рантаймов):
|
|
95
|
+
// an OCCUPIED memory slot gates the AUTOMATIC lever at peer birth — zero window of
|
|
96
|
+
// a parallel, uncurated native store (the criterion). Empty slot → native memory is
|
|
97
|
+
// legitimate, untouched. Best-effort with a LOUD warn (a lever hiccup must not kill
|
|
98
|
+
// the provision — the operator verb `iapeer native-memory off` is the repair).
|
|
99
|
+
try {
|
|
100
|
+
const { readMemoryProvider } = await import('../status/index.ts')
|
|
101
|
+
if (readMemoryProvider(env)) {
|
|
102
|
+
const { applyNativeMemory, preTrustCodexCwd } = await import('../launch/nativeMemory.ts')
|
|
103
|
+
for (const o of applyNativeMemory(cwd, profile.runtimes, 'off')) {
|
|
104
|
+
if (o.state === 'failed') {
|
|
105
|
+
opts.warn?.(
|
|
106
|
+
`native-memory lever (${o.runtime}) FAILED for "${profile.personality}": ${o.detail} — ` +
|
|
107
|
+
`the peer may accumulate a parallel native store; repair: iapeer native-memory off --peer ${profile.personality}`,
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// Codex reads project-local config only for a TRUSTED cwd — pre-trust the
|
|
112
|
+
// newborn so the lever acts from the FIRST session (contract requirement),
|
|
113
|
+
// not from whenever the boot dialog's trust grant takes effect.
|
|
114
|
+
if (profile.runtimes.includes('codex')) {
|
|
115
|
+
const t = preTrustCodexCwd(cwd, env)
|
|
116
|
+
if (t.state === 'failed') {
|
|
117
|
+
opts.warn?.(
|
|
118
|
+
`codex pre-trust FAILED for "${profile.personality}": ${t.detail} — ` +
|
|
119
|
+
`the lever may be inert until the first boot's trust dialog`,
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
} catch (e) {
|
|
125
|
+
opts.warn?.(`native-memory birth-time hook failed: ${e instanceof Error ? e.message : String(e)}`)
|
|
126
|
+
}
|
|
127
|
+
|
|
94
128
|
// wake_policy:"ephemeral" sanity (M2 edge cases — warn, don't refuse: the policy
|
|
95
129
|
// lives in the hand-editable local profile, provision just surfaces the mismatch):
|
|
96
130
|
// • + interfaces.telegram: ephemeral WINS in resolveWakeMode (explicit policy
|
|
@@ -158,3 +158,44 @@ describe('provisionPeer ephemeral warnings', () => {
|
|
|
158
158
|
expect(warns.some(w => /ephemeral/.test(w) && /inert|launchd/i.test(w))).toBe(true)
|
|
159
159
|
})
|
|
160
160
|
})
|
|
161
|
+
|
|
162
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
163
|
+
// Birth-time native-memory lever (slot contract §Native-память): an OCCUPIED
|
|
164
|
+
// slot gates the automatic lever at provision; empty slot → untouched.
|
|
165
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
describe('provisionPeer birth-time native-memory lever', () => {
|
|
168
|
+
test('slot OCCUPIED → claude+codex levers written + codex cwd pre-trusted', async () => {
|
|
169
|
+
const root = mkTmp()
|
|
170
|
+
const env = envFor(root)
|
|
171
|
+
// occupy the slot (as the provider's init would)
|
|
172
|
+
mkdirSync(join(root, 'iapeer'), { recursive: true })
|
|
173
|
+
writeFileSync(
|
|
174
|
+
join(root, 'iapeer', 'memory-provider.json'),
|
|
175
|
+
JSON.stringify({ provider: 'iapeer-memory', package: '@agfpd/iapeer-memory', version: '0.1.0', registeredAt: 'x' }),
|
|
176
|
+
)
|
|
177
|
+
const cwd = join(root, 'wnat')
|
|
178
|
+
mkdirSync(join(cwd, '.iapeer'), { recursive: true })
|
|
179
|
+
writeFileSync(
|
|
180
|
+
peerProfilePath(cwd),
|
|
181
|
+
JSON.stringify({ personality: 'wnat', runtime: 'claude', runtimes: ['claude', 'codex'] }),
|
|
182
|
+
)
|
|
183
|
+
await provisionPeer({ cwd, runtime: 'claude', env })
|
|
184
|
+
expect(JSON.parse(readFileSync(join(cwd, '.claude', 'settings.json'), 'utf8')).autoMemoryEnabled).toBe(false)
|
|
185
|
+
expect(readFileSync(join(cwd, '.codex', 'config.toml'), 'utf8')).toContain('memories = false')
|
|
186
|
+
// codex newborn pre-trusted in the GLOBAL codex config (HOME-scoped → temp root).
|
|
187
|
+
// Codex keys trust on the RESOLVED real path (macOS /var → /private/var), so the
|
|
188
|
+
// entry must carry realpath(cwd) — the literal-path miss was a live-caught bug.
|
|
189
|
+
const { realpathSync } = await import('fs')
|
|
190
|
+
expect(readFileSync(join(root, '.codex', 'config.toml'), 'utf8')).toContain(`[projects."${realpathSync(cwd)}"]`)
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
test('slot EMPTY → native memory untouched (legitimate without a provider)', async () => {
|
|
194
|
+
const root = mkTmp()
|
|
195
|
+
const env = envFor(root)
|
|
196
|
+
const cwd = join(root, 'wbare')
|
|
197
|
+
await provisionPeer({ cwd, runtime: 'claude', env })
|
|
198
|
+
expect(existsSync(join(cwd, '.claude', 'settings.json'))).toBe(false)
|
|
199
|
+
expect(existsSync(join(cwd, '.codex', 'config.toml'))).toBe(false)
|
|
200
|
+
})
|
|
201
|
+
})
|