pi-codex-marketplace 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +134 -0
- package/extensions/pi/git-registration.ts +138 -0
- package/extensions/pi/index.ts +293 -0
- package/extensions/pi/installation.ts +90 -0
- package/extensions/pi/journal.ts +80 -0
- package/extensions/pi/lifecycle.ts +285 -0
- package/extensions/pi/registration.ts +143 -0
- package/extensions/pi/scope-overrides.ts +170 -0
- package/package.json +60 -0
- package/src/barrier/global-barrier.ts +105 -0
- package/src/bridge-state/atomic.ts +237 -0
- package/src/bridge-state/index.ts +5 -0
- package/src/bridge-state/migrate.ts +261 -0
- package/src/bridge-state/paths.ts +75 -0
- package/src/bridge-state/repair.ts +185 -0
- package/src/bridge-state/schema.ts +70 -0
- package/src/bridge-state/store.ts +489 -0
- package/src/bridge-state/types.ts +170 -0
- package/src/cache/index.ts +2 -0
- package/src/cache/paths.ts +42 -0
- package/src/cache/source-cache.ts +365 -0
- package/src/compatibility/index.ts +1 -0
- package/src/compatibility/profile.ts +328 -0
- package/src/installation/flow.ts +443 -0
- package/src/installation/index.ts +1 -0
- package/src/installation/inspection.ts +129 -0
- package/src/journal/active-chains.ts +99 -0
- package/src/journal/index.ts +3 -0
- package/src/journal/journal.ts +215 -0
- package/src/journal/types.ts +49 -0
- package/src/lifecycle/index.ts +5 -0
- package/src/lifecycle/rebind.ts +290 -0
- package/src/lifecycle/refresh.ts +407 -0
- package/src/lifecycle/removal.ts +457 -0
- package/src/lifecycle/update-plan.ts +222 -0
- package/src/lifecycle/update.ts +303 -0
- package/src/projection/collision.ts +120 -0
- package/src/projection/effective-state.ts +182 -0
- package/src/projection/index.ts +4 -0
- package/src/projection/overrides.ts +230 -0
- package/src/projection/project.ts +359 -0
- package/src/reconciliation/startup.ts +144 -0
- package/src/registration/budget.ts +28 -0
- package/src/registration/catalog.ts +224 -0
- package/src/registration/contained.ts +140 -0
- package/src/registration/fence.ts +86 -0
- package/src/registration/findings.ts +188 -0
- package/src/registration/flow.ts +619 -0
- package/src/registration/git-acquisition.ts +481 -0
- package/src/registration/git-flow.ts +654 -0
- package/src/registration/git-locator.ts +380 -0
- package/src/registration/git-selector.ts +279 -0
- package/src/registration/index.ts +16 -0
- package/src/registration/receipt.ts +305 -0
- package/src/registration/registration.ts +102 -0
- package/src/registration/snapshot.ts +382 -0
- package/src/registration/source-key.ts +111 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scope Override lifecycle — Project Scope records that explicitly suppress inherited
|
|
3
|
+
* Global Scope records without modifying them.
|
|
4
|
+
* See CONTEXT.md: Scope Override, Project Trust, Lifecycle Operation, Attempt Fence, State Revision.
|
|
5
|
+
*
|
|
6
|
+
* A Registration override suppresses its marketplace subtree; an Installation override
|
|
7
|
+
* suppresses only that single Plugin. Removing either reveals the inherited record again at
|
|
8
|
+
* the next read — inheritance is restored by Effective State recomputation, never by rewriting
|
|
9
|
+
* the global document. Both operations are Project Scope Lifecycle Operations: they require
|
|
10
|
+
* Project Trust, run under the scope Attempt Fence, bind the exact State Revision at admission,
|
|
11
|
+
* commit atomically to the project document only, and produce an immutable Attempt Receipt.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { readBridgeState, commitBridgeState } from '../bridge-state/store.js';
|
|
15
|
+
import type { BridgeState, ScopeOverride } from '../bridge-state/types.js';
|
|
16
|
+
import { acquireAttemptFence } from '../registration/fence.js';
|
|
17
|
+
import { CODE, RULE, blocking, type ValidationFinding } from '../registration/findings.js';
|
|
18
|
+
import { createReceipt, type AttemptReceipt } from '../registration/receipt.js';
|
|
19
|
+
import { appendReceipt } from '../journal/journal.js';
|
|
20
|
+
|
|
21
|
+
export type OverrideKind = ScopeOverride['kind'];
|
|
22
|
+
|
|
23
|
+
export interface ScopeOverrideFlowOptions {
|
|
24
|
+
cwd?: string;
|
|
25
|
+
agentDir?: string;
|
|
26
|
+
projectTrusted?: boolean;
|
|
27
|
+
fenceTimeoutMs?: number;
|
|
28
|
+
/** Integration synchronization seam; production callers leave this undefined. */
|
|
29
|
+
beforeCommit?: () => void | Promise<void>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type OverrideOutcome =
|
|
33
|
+
| { status: 'completed'; receipt: AttemptReceipt; newRevision: string }
|
|
34
|
+
| { status: 'blocked'; findings: ValidationFinding[]; receipt: AttemptReceipt }
|
|
35
|
+
| { status: 'rejected-as-stale'; receipt: AttemptReceipt }
|
|
36
|
+
| { status: 'persistence-failed'; receipt: AttemptReceipt; isIndeterminate: boolean };
|
|
37
|
+
|
|
38
|
+
const OPERATION_NAME = {
|
|
39
|
+
createRegistration: 'Registration Override Creation',
|
|
40
|
+
createInstallation: 'Installation Override Creation',
|
|
41
|
+
removeRegistration: 'Registration Override Removal',
|
|
42
|
+
removeInstallation: 'Installation Override Removal',
|
|
43
|
+
} as const;
|
|
44
|
+
|
|
45
|
+
function operationName(kind: OverrideKind, direction: 'create' | 'remove'): string {
|
|
46
|
+
if (kind === 'registration') return direction === 'create' ? OPERATION_NAME.createRegistration : OPERATION_NAME.removeRegistration;
|
|
47
|
+
return direction === 'create' ? OPERATION_NAME.createInstallation : OPERATION_NAME.removeInstallation;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function admissionFinding(code: string, rule: string, outcome: string, target: ValidationFinding['target']): ValidationFinding {
|
|
51
|
+
return blocking({ code, rule, target, pointer: '', outcome, scope: 'project', phase: 'admission' });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function blocked(operation: string, targetId: string, revision: string, findings: ValidationFinding[], opts: ScopeOverrideFlowOptions = {}): Promise<OverrideOutcome> {
|
|
55
|
+
const receipt = createReceipt({
|
|
56
|
+
operation,
|
|
57
|
+
scope: 'project',
|
|
58
|
+
trigger: `${operation.toLowerCase()} ${targetId}`,
|
|
59
|
+
expectedStateRevision: revision,
|
|
60
|
+
summary: 'Blocked',
|
|
61
|
+
findings,
|
|
62
|
+
});
|
|
63
|
+
await appendReceipt('project', receipt, opts);
|
|
64
|
+
return {
|
|
65
|
+
status: 'blocked',
|
|
66
|
+
findings,
|
|
67
|
+
receipt,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function persistenceFailed(operation: string, targetId: string, isIndeterminate: boolean, error?: string, opts: ScopeOverrideFlowOptions = {}): Promise<OverrideOutcome> {
|
|
72
|
+
const receipt = createReceipt({
|
|
73
|
+
operation,
|
|
74
|
+
scope: 'project',
|
|
75
|
+
trigger: `${operation.toLowerCase()} ${targetId}`,
|
|
76
|
+
expectedStateRevision: '?',
|
|
77
|
+
summary: isIndeterminate ? 'Persistence Indeterminate' : 'Persistence Failed',
|
|
78
|
+
findings: isIndeterminate
|
|
79
|
+
? [admissionFinding(CODE.PERSISTENCE_INDETERMINATE, 'PERSIST-01', error ?? 'Bridge State is not readable', 'attempt')]
|
|
80
|
+
: [],
|
|
81
|
+
});
|
|
82
|
+
await appendReceipt('project', receipt, opts);
|
|
83
|
+
return {
|
|
84
|
+
status: 'persistence-failed',
|
|
85
|
+
isIndeterminate,
|
|
86
|
+
receipt,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function runOverrideOperation(
|
|
91
|
+
direction: 'create' | 'remove',
|
|
92
|
+
kind: OverrideKind,
|
|
93
|
+
targetId: string,
|
|
94
|
+
opts: ScopeOverrideFlowOptions,
|
|
95
|
+
/** Admission validation over both authoritative documents; returns the blocking denial, if any. */
|
|
96
|
+
validate: (projectState: BridgeState, globalState: BridgeState) => ValidationFinding | undefined,
|
|
97
|
+
): Promise<OverrideOutcome> {
|
|
98
|
+
const operation = operationName(kind, direction);
|
|
99
|
+
if (opts.projectTrusted !== true) {
|
|
100
|
+
return blocked(operation, targetId, '?', [
|
|
101
|
+
admissionFinding(CODE.PROJECT_TRUST_DENIED, RULE.PROJECT_TRUST_DENIED, 'Project Trust is not granted by the Pi host; no Project Scope Lifecycle Operation may mutate Bridge State', kind === 'registration' ? 'registration' : 'installation'),
|
|
102
|
+
], opts);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const storeOpts = { cwd: opts.cwd, agentDir: opts.agentDir };
|
|
106
|
+
const fence = await acquireAttemptFence('project', { ...storeOpts, fenceTimeoutMs: opts.fenceTimeoutMs, projectTrusted: opts.projectTrusted });
|
|
107
|
+
if (!fence.ok) return blocked(operation, targetId, '?', [fence.finding!], opts);
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const projectRead = await readBridgeState('project', storeOpts);
|
|
111
|
+
const globalRead = await readBridgeState('global', storeOpts);
|
|
112
|
+
if (projectRead.status !== 'ok' && projectRead.status !== 'missing') {
|
|
113
|
+
return persistenceFailed(operation, targetId, true, projectRead.error, opts);
|
|
114
|
+
}
|
|
115
|
+
if (globalRead.status !== 'ok' && globalRead.status !== 'missing') {
|
|
116
|
+
return persistenceFailed(operation, targetId, true, globalRead.error, opts);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const projectState = projectRead.state!;
|
|
120
|
+
const denial = validate(projectState, globalRead.state!);
|
|
121
|
+
if (denial) return blocked(operation, targetId, projectState.stateRevision, [denial], opts);
|
|
122
|
+
|
|
123
|
+
await opts.beforeCommit?.();
|
|
124
|
+
|
|
125
|
+
const write = await commitBridgeState(
|
|
126
|
+
'project',
|
|
127
|
+
(state) => ({
|
|
128
|
+
...state,
|
|
129
|
+
scopeOverrides:
|
|
130
|
+
direction === 'create'
|
|
131
|
+
? [...state.scopeOverrides.filter((item) => !(item.kind === kind && item.targetId === targetId)), { kind, targetId }]
|
|
132
|
+
: state.scopeOverrides.filter((item) => !(item.kind === kind && item.targetId === targetId)),
|
|
133
|
+
}),
|
|
134
|
+
{ ...storeOpts, lockTimeoutMs: opts.fenceTimeoutMs, expectedStateRevision: projectState.stateRevision },
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
if (write.isStale) {
|
|
138
|
+
const receipt = createReceipt({
|
|
139
|
+
operation,
|
|
140
|
+
scope: 'project',
|
|
141
|
+
trigger: `${operation.toLowerCase()} ${targetId}`,
|
|
142
|
+
expectedStateRevision: projectState.stateRevision,
|
|
143
|
+
observedStateRevision: write.observedRevision,
|
|
144
|
+
summary: 'Rejected as Stale',
|
|
145
|
+
findings: [blocking({ code: CODE.REJECTED_AS_STALE, rule: RULE.REJECTED_AS_STALE, target: kind === 'registration' ? 'registration' : 'installation', pointer: '', outcome: `State Revision changed after ${operation} admission; re-run the lifecycle operation`, scope: 'project', phase: 'persistence' })],
|
|
146
|
+
});
|
|
147
|
+
await appendReceipt('project', receipt, opts);
|
|
148
|
+
return {
|
|
149
|
+
status: 'rejected-as-stale',
|
|
150
|
+
receipt,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
if (!write.success) return persistenceFailed(operation, targetId, write.isIndeterminate ?? false, write.error, opts);
|
|
154
|
+
|
|
155
|
+
const receipt = createReceipt({
|
|
156
|
+
operation,
|
|
157
|
+
scope: 'project',
|
|
158
|
+
trigger: `${operation.toLowerCase()} ${targetId}`,
|
|
159
|
+
expectedStateRevision: projectState.stateRevision,
|
|
160
|
+
targetStateRevision: write.newRevision,
|
|
161
|
+
observedStateRevision: write.newRevision,
|
|
162
|
+
summary: 'Completed',
|
|
163
|
+
stateChanged: true,
|
|
164
|
+
});
|
|
165
|
+
await appendReceipt('project', receipt, opts);
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
status: 'completed',
|
|
169
|
+
newRevision: write.newRevision!,
|
|
170
|
+
receipt,
|
|
171
|
+
};
|
|
172
|
+
} finally {
|
|
173
|
+
fence.handle!.release();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Create one Scope Override suppressing an inherited Global record.
|
|
179
|
+
* The target must exist in the inherited Global Scope and must not already be suppressed.
|
|
180
|
+
*/
|
|
181
|
+
export async function createScopeOverride(
|
|
182
|
+
kind: OverrideKind,
|
|
183
|
+
targetId: string,
|
|
184
|
+
opts: ScopeOverrideFlowOptions = {},
|
|
185
|
+
): Promise<OverrideOutcome> {
|
|
186
|
+
return runOverrideOperation('create', kind, targetId, opts, (projectState, globalState) => {
|
|
187
|
+
if (projectState.scopeOverrides.some((item) => item.kind === kind && item.targetId === targetId)) {
|
|
188
|
+
return admissionFinding(
|
|
189
|
+
CODE.SCOPE_OVERRIDE_ALREADY_PRESENT,
|
|
190
|
+
RULE.SCOPE_OVERRIDE_ALREADY_PRESENT,
|
|
191
|
+
`A ${kind} Scope Override for '${targetId}' already exists in Project Scope`,
|
|
192
|
+
kind === 'registration' ? 'registration' : 'installation',
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
const exists = kind === 'registration'
|
|
196
|
+
? globalState.registrations.some((registration) => registration.id === targetId)
|
|
197
|
+
: globalState.installations.some((installation) => installation.id === targetId);
|
|
198
|
+
if (!exists) {
|
|
199
|
+
return admissionFinding(
|
|
200
|
+
CODE.SCOPE_OVERRIDE_TARGET_NOT_FOUND,
|
|
201
|
+
RULE.SCOPE_OVERRIDE_TARGET_NOT_FOUND,
|
|
202
|
+
`Suppression target '${targetId}' does not exist in the inherited Global Scope; Scope Overrides suppress inherited records only`,
|
|
203
|
+
kind === 'registration' ? 'registration' : 'installation',
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
return undefined;
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Remove one Scope Override; inheritance is restored by Effective State recomputation without
|
|
212
|
+
* rewriting the global document.
|
|
213
|
+
*/
|
|
214
|
+
export async function removeScopeOverride(
|
|
215
|
+
kind: OverrideKind,
|
|
216
|
+
targetId: string,
|
|
217
|
+
opts: ScopeOverrideFlowOptions = {},
|
|
218
|
+
): Promise<OverrideOutcome> {
|
|
219
|
+
return runOverrideOperation('remove', kind, targetId, opts, (projectState) => {
|
|
220
|
+
if (!projectState.scopeOverrides.some((item) => item.kind === kind && item.targetId === targetId)) {
|
|
221
|
+
return admissionFinding(
|
|
222
|
+
CODE.SCOPE_OVERRIDE_TARGET_NOT_FOUND,
|
|
223
|
+
RULE.SCOPE_OVERRIDE_TARGET_NOT_FOUND,
|
|
224
|
+
`No ${kind} Scope Override for '${targetId}' exists in Project Scope`,
|
|
225
|
+
kind === 'registration' ? 'registration' : 'installation',
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
return undefined;
|
|
229
|
+
});
|
|
230
|
+
}
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Projection — admits Effective State Installations as Projected Plugins and exposes their
|
|
3
|
+
* surviving skills to Pi through Pi's resource-discovery seam at their original snapshot paths.
|
|
4
|
+
* See CONTEXT.md: Projected Plugin, Projected Skill, Runtime Skill Collision, Source Drift,
|
|
5
|
+
* Skill Availability, Runtime Application.
|
|
6
|
+
*
|
|
7
|
+
* Whole-Plugin Blocking Findings (Source Drift, Unavailable Entry, failed classification)
|
|
8
|
+
* deny an entire Plugin; Runtime Skill Collision denies only individual skill candidates and
|
|
9
|
+
* never changes Plugin classification or Projected Plugin determination. Skills are projected
|
|
10
|
+
* directly from the retained snapshot tree — never copied or converted — while the Bridge holds
|
|
11
|
+
* provenance. Only independent host evidence may establish Skill Availability as Available,
|
|
12
|
+
* and a host-verifiable reload through Bridge re-entry is what makes a Runtime Application
|
|
13
|
+
* Applied.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { loadSkillsFromDir } from '@earendil-works/pi-coding-agent';
|
|
17
|
+
|
|
18
|
+
import type { BridgeState, Scope } from '../bridge-state/types.js';
|
|
19
|
+
import {
|
|
20
|
+
computeEffectiveState,
|
|
21
|
+
type EffectiveInstallation,
|
|
22
|
+
type EffectiveRegistration,
|
|
23
|
+
} from './effective-state.js';
|
|
24
|
+
import {
|
|
25
|
+
resolveRuntimeSkillCollisions,
|
|
26
|
+
type CollisionResolution,
|
|
27
|
+
type SkillCandidate,
|
|
28
|
+
} from './collision.js';
|
|
29
|
+
import { inspectMarketplaceEntries, type MarketplaceInspection } from '../installation/inspection.js';
|
|
30
|
+
import { CODE, RULE, blocking, sortFindings, type ValidationFinding } from '../registration/findings.js';
|
|
31
|
+
import { createReceipt, type AttemptReceipt } from '../registration/receipt.js';
|
|
32
|
+
import { appendReceipt } from '../journal/journal.js';
|
|
33
|
+
|
|
34
|
+
export interface ProjectionOptions {
|
|
35
|
+
cwd?: string;
|
|
36
|
+
agentDir?: string;
|
|
37
|
+
projectTrusted?: boolean;
|
|
38
|
+
/** Exact names already claimed by pre-existing Pi skills (host-owned namespace layer). */
|
|
39
|
+
piSkillNames?: string[];
|
|
40
|
+
/** Injection seam for tests; defaults to live local-source inspection. */
|
|
41
|
+
inspectRegistration?: (registration: EffectiveRegistration) => MarketplaceInspection;
|
|
42
|
+
/** Independent host evidence establishing Skill Availability as Available. */
|
|
43
|
+
hostAvailabilityEvidence?: (skillName: string) => boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** A whole-Plugin blocking denial that must hold a Runtime Application short of Applied. */
|
|
47
|
+
export interface RuntimeApplicationInput {
|
|
48
|
+
/** Exact State Revision the runtime is expected to re-enter at. */
|
|
49
|
+
stateRevision?: string;
|
|
50
|
+
scope?: Scope;
|
|
51
|
+
/** Whole-Plugin Blocking Findings from projection; any one of them denies Applied. */
|
|
52
|
+
wholePluginFindings?: ValidationFinding[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export type SkillAvailability = 'available' | 'snapshot-eligible' | 'unverified' | 'unavailable';
|
|
56
|
+
|
|
57
|
+
export interface ProjectedSkillView {
|
|
58
|
+
name: string;
|
|
59
|
+
skillId: string;
|
|
60
|
+
pluginId: string;
|
|
61
|
+
status: 'projected' | 'unavailable-collision';
|
|
62
|
+
availability: SkillAvailability;
|
|
63
|
+
/** Original snapshot location of SKILL.md exposed through Pi discovery (no copy). */
|
|
64
|
+
discoveryPath?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface ProjectedPluginView {
|
|
68
|
+
pluginId: string;
|
|
69
|
+
manifestName?: string;
|
|
70
|
+
installationId: string;
|
|
71
|
+
registrationId?: string;
|
|
72
|
+
sourceScope: 'global' | 'project';
|
|
73
|
+
skills: ProjectedSkillView[];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface DeniedPluginView {
|
|
77
|
+
installationId: string;
|
|
78
|
+
pluginId?: string;
|
|
79
|
+
/** Whole-Plugin blocking denial; never a skill-granular collision. */
|
|
80
|
+
reason: ValidationFinding;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface ProjectionResult {
|
|
84
|
+
plugins: ProjectedPluginView[];
|
|
85
|
+
denied: DeniedPluginView[];
|
|
86
|
+
/** Skill-granular collision denials only; whole-plugin denials live in `denied`. */
|
|
87
|
+
findings: ValidationFinding[];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function driftFinding(scope: 'global' | 'project'): ValidationFinding {
|
|
91
|
+
return blocking({
|
|
92
|
+
code: CODE.SOURCE_DRIFT,
|
|
93
|
+
rule: RULE.SOURCE_DRIFT,
|
|
94
|
+
target: 'registration',
|
|
95
|
+
pointer: '',
|
|
96
|
+
outcome: 'Source Drift: the registered Validation Snapshot no longer matches the source tree; affected Installations cannot become Projected Plugins until Marketplace Refresh produces an Update Candidate',
|
|
97
|
+
scope,
|
|
98
|
+
phase: 'validation',
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function collisionFinding(scope: 'global' | 'project', skillId: string): ValidationFinding {
|
|
103
|
+
return blocking({
|
|
104
|
+
code: CODE.RUNTIME_SKILL_COLLISION,
|
|
105
|
+
rule: RULE.RUNTIME_SKILL_COLLISION,
|
|
106
|
+
target: 'skill',
|
|
107
|
+
pointer: skillId,
|
|
108
|
+
outcome: `Runtime Skill Collision: skill '${skillId}' is unavailable because another candidate claims this exact Skill Descriptor name`,
|
|
109
|
+
scope,
|
|
110
|
+
phase: 'validation',
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Compute the projection of one Effective State. Read-only: it never mutates either Bridge
|
|
116
|
+
* State document and never persists anything.
|
|
117
|
+
*/
|
|
118
|
+
export function projectEffectiveState(
|
|
119
|
+
globalState: BridgeState,
|
|
120
|
+
projectState: BridgeState,
|
|
121
|
+
opts: ProjectionOptions = {},
|
|
122
|
+
): ProjectionResult {
|
|
123
|
+
const effective = computeEffectiveState(globalState, projectState, { projectTrusted: opts.projectTrusted });
|
|
124
|
+
const denied: DeniedPluginView[] = [];
|
|
125
|
+
const findings: ValidationFinding[] = [];
|
|
126
|
+
|
|
127
|
+
const inspections = new Map<string, MarketplaceInspection>();
|
|
128
|
+
const inspect = opts.inspectRegistration ?? ((registration: EffectiveRegistration) => inspectMarketplaceEntries(registration, registration.sourceScope));
|
|
129
|
+
const inspectionFor = (registration: EffectiveRegistration): MarketplaceInspection => {
|
|
130
|
+
const key = `${registration.sourceScope}:${registration.id}`;
|
|
131
|
+
let value = inspections.get(key);
|
|
132
|
+
if (!value) {
|
|
133
|
+
value = inspect(registration);
|
|
134
|
+
inspections.set(key, value);
|
|
135
|
+
}
|
|
136
|
+
return value;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
interface AdmittedPlugin {
|
|
140
|
+
pluginId: string;
|
|
141
|
+
manifestName?: string;
|
|
142
|
+
installationId: string;
|
|
143
|
+
registrationId?: string;
|
|
144
|
+
sourceScope: 'global' | 'project';
|
|
145
|
+
skills: Array<{ name: string; skillId: string; path: string }>;
|
|
146
|
+
}
|
|
147
|
+
const admitted: AdmittedPlugin[] = [];
|
|
148
|
+
|
|
149
|
+
for (const installation of effective.installations) {
|
|
150
|
+
const deny = (pluginId: string | undefined, reason: ValidationFinding): void => {
|
|
151
|
+
denied.push({ installationId: installation.id, pluginId, reason });
|
|
152
|
+
};
|
|
153
|
+
const registration = effective.registrations.find(
|
|
154
|
+
(item) => item.id === installation.registrationId && item.sourceScope === installation.sourceScope,
|
|
155
|
+
);
|
|
156
|
+
if (!installation.registrationId || !registration) {
|
|
157
|
+
deny(installation.pluginId, blocking({
|
|
158
|
+
code: CODE.INSTALLATION_NOT_FOUND,
|
|
159
|
+
rule: RULE.INSTALLATION_NOT_FOUND,
|
|
160
|
+
target: 'installation',
|
|
161
|
+
pointer: '',
|
|
162
|
+
outcome: `Installation '${installation.id}' references Registration '${installation.registrationId ?? '?'}' which is not part of Effective State`,
|
|
163
|
+
scope: installation.sourceScope,
|
|
164
|
+
phase: 'validation',
|
|
165
|
+
}));
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
const inspection = inspectionFor(registration);
|
|
169
|
+
// Source Drift: the registered snapshot no longer matches the live tree — fail closed.
|
|
170
|
+
// Registrations persist the base tree fingerprint from their preflight (buildLocalSnapshot);
|
|
171
|
+
// drift compares only against the same kind of fingerprint, never the activation-bound one.
|
|
172
|
+
if (
|
|
173
|
+
registration.validationSnapshot &&
|
|
174
|
+
inspection.treeFingerprint !== undefined &&
|
|
175
|
+
inspection.treeFingerprint !== registration.validationSnapshot
|
|
176
|
+
) {
|
|
177
|
+
deny(installation.pluginId, driftFinding(installation.sourceScope));
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
const entryPointer = installation.marketplaceEntryId
|
|
181
|
+
? installation.marketplaceEntryId.slice(installation.marketplaceEntryId.indexOf('/plugins/'))
|
|
182
|
+
: undefined;
|
|
183
|
+
const inspected = inspection.entries.find((item) =>
|
|
184
|
+
entryPointer !== undefined && `${inspection.marketplaceId}${item.entry.entryId}` === installation.marketplaceEntryId,
|
|
185
|
+
);
|
|
186
|
+
if (!inspection.marketplaceId || !inspected) {
|
|
187
|
+
deny(installation.pluginId, blocking({
|
|
188
|
+
code: CODE.INSTALLATION_NOT_FOUND,
|
|
189
|
+
rule: RULE.INSTALLATION_NOT_FOUND,
|
|
190
|
+
target: 'entry',
|
|
191
|
+
pointer: installation.marketplaceEntryId ?? '',
|
|
192
|
+
outcome: `Installed Plugin '${installation.id}' cannot be resolved to its Marketplace Entry in the current snapshot`,
|
|
193
|
+
scope: installation.sourceScope,
|
|
194
|
+
phase: 'validation',
|
|
195
|
+
}));
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
// Whole-Plugin admission: any Whole-Plugin Blocking Finding denies the entire Plugin.
|
|
199
|
+
if (inspected.unavailableReason || !inspected.plugin || inspected.findings.some((f) => f.classification === 'blocking')) {
|
|
200
|
+
deny(installation.pluginId, inspected.findings.find((f) => f.classification === 'blocking') ?? blocking({
|
|
201
|
+
code: CODE.PLUGIN_MANIFEST_INVALID,
|
|
202
|
+
rule: RULE.PLUGIN_MANIFEST_INVALID,
|
|
203
|
+
target: 'plugin',
|
|
204
|
+
pointer: '',
|
|
205
|
+
outcome: inspected.unavailableReason ?? 'Plugin is not activatable',
|
|
206
|
+
scope: installation.sourceScope,
|
|
207
|
+
phase: 'validation',
|
|
208
|
+
}));
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
admitted.push({
|
|
212
|
+
pluginId: inspected.plugin.id,
|
|
213
|
+
manifestName: inspected.plugin.manifestName,
|
|
214
|
+
installationId: installation.id,
|
|
215
|
+
registrationId: registration.id,
|
|
216
|
+
sourceScope: installation.sourceScope,
|
|
217
|
+
skills: inspected.plugin.skills.map((skill) => ({
|
|
218
|
+
name: skill.name,
|
|
219
|
+
skillId: `${inspected.plugin!.id}/${skill.name}`,
|
|
220
|
+
path: joinPath(skill.path, 'SKILL.md'),
|
|
221
|
+
})),
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Runtime Skill Collision resolution across Pi → Project Scope → Global Scope.
|
|
226
|
+
const candidates: SkillCandidate[] = [
|
|
227
|
+
...(opts.piSkillNames ?? []).map((name): SkillCandidate => ({ layer: 'pi', name, skillId: `pi/${name}`, pluginId: '(pi)' })),
|
|
228
|
+
...admitted.flatMap((plugin): SkillCandidate[] => plugin.skills.map((skill) => ({
|
|
229
|
+
layer: plugin.sourceScope,
|
|
230
|
+
name: skill.name,
|
|
231
|
+
skillId: skill.skillId,
|
|
232
|
+
pluginId: plugin.pluginId,
|
|
233
|
+
}))),
|
|
234
|
+
];
|
|
235
|
+
const resolution: CollisionResolution = resolveRuntimeSkillCollisions(candidates);
|
|
236
|
+
const survivedIds = new Set(resolution.survivors.map((s) => s.skillId));
|
|
237
|
+
for (const info of resolution.findings) {
|
|
238
|
+
for (const skillId of info.unavailableSkillIds) {
|
|
239
|
+
const owner = admitted.find((plugin) => plugin.skills.some((skill) => skill.skillId === skillId));
|
|
240
|
+
if (owner) findings.push(collisionFinding(owner.sourceScope, skillId));
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const plugins: ProjectedPluginView[] = admitted.map((plugin) => ({
|
|
245
|
+
pluginId: plugin.pluginId,
|
|
246
|
+
manifestName: plugin.manifestName,
|
|
247
|
+
installationId: plugin.installationId,
|
|
248
|
+
registrationId: plugin.registrationId,
|
|
249
|
+
sourceScope: plugin.sourceScope,
|
|
250
|
+
skills: plugin.skills.map((skill): ProjectedSkillView => {
|
|
251
|
+
if (!survivedIds.has(skill.skillId)) {
|
|
252
|
+
return { name: skill.name, skillId: skill.skillId, pluginId: plugin.pluginId, status: 'unavailable-collision', availability: 'unavailable' };
|
|
253
|
+
}
|
|
254
|
+
// Pi resource-discovery seam: expose the skill at its original snapshot path.
|
|
255
|
+
let discoveryPath: string | undefined;
|
|
256
|
+
try {
|
|
257
|
+
const loaded = loadSkillsFromDir({ dir: dirnamePath(skill.path), source: `codex-marketplace:${plugin.pluginId}` });
|
|
258
|
+
const match = loaded.skills.find((item) => item.name === skill.name) ?? loaded.skills[0];
|
|
259
|
+
discoveryPath = match?.filePath;
|
|
260
|
+
} catch {
|
|
261
|
+
discoveryPath = undefined;
|
|
262
|
+
}
|
|
263
|
+
if (opts.hostAvailabilityEvidence?.(skill.name)) {
|
|
264
|
+
return { name: skill.name, skillId: skill.skillId, pluginId: plugin.pluginId, status: 'projected', availability: 'available', discoveryPath };
|
|
265
|
+
}
|
|
266
|
+
return {
|
|
267
|
+
name: skill.name,
|
|
268
|
+
skillId: skill.skillId,
|
|
269
|
+
pluginId: plugin.pluginId,
|
|
270
|
+
status: 'projected',
|
|
271
|
+
// Snapshot-bound eligibility only: Available requires independent host evidence.
|
|
272
|
+
availability: discoveryPath ? 'snapshot-eligible' : 'unverified',
|
|
273
|
+
discoveryPath,
|
|
274
|
+
};
|
|
275
|
+
}),
|
|
276
|
+
}));
|
|
277
|
+
|
|
278
|
+
return { plugins, denied, findings: sortFindings(findings) };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function joinPath(directory: string, file: string): string {
|
|
282
|
+
return directory.endsWith('/') ? `${directory}${file}` : `${directory}/${file}`;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function dirnamePath(filePath: string): string {
|
|
286
|
+
const index = filePath.lastIndexOf('/');
|
|
287
|
+
return index > 0 ? filePath.slice(0, index) : filePath;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export interface RuntimeApplicationInput {
|
|
291
|
+
/** Exact State Revision the runtime is expected to re-enter at. */
|
|
292
|
+
stateRevision?: string;
|
|
293
|
+
scope?: Scope;
|
|
294
|
+
cwd?: string;
|
|
295
|
+
agentDir?: string;
|
|
296
|
+
recoversReceiptId?: string;
|
|
297
|
+
/** Whole-Plugin Blocking Findings from projection; any one of them denies Applied. */
|
|
298
|
+
wholePluginFindings?: ValidationFinding[];
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export interface RuntimeApplicationOutcome {
|
|
302
|
+
outcome: 'applied' | 'pending-application';
|
|
303
|
+
receipt: AttemptReceipt;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Runtime Application seam: participation in Pi becomes Applied only through
|
|
308
|
+
* host-verifiable Bridge re-entry at the expected State Revision with no whole-Plugin
|
|
309
|
+
* Blocking Finding; anything less leaves Pending Application or stays Blocked.
|
|
310
|
+
*/
|
|
311
|
+
export async function requestRuntimeApplication(
|
|
312
|
+
verifyReload: () => Promise<boolean> | boolean,
|
|
313
|
+
input: RuntimeApplicationInput = {},
|
|
314
|
+
): Promise<RuntimeApplicationOutcome> {
|
|
315
|
+
const scope: Scope = input.scope ?? 'project';
|
|
316
|
+
const stateRevision = input.stateRevision ?? '-';
|
|
317
|
+
const opts = { cwd: input.cwd, agentDir: input.agentDir };
|
|
318
|
+
|
|
319
|
+
if (input.wholePluginFindings?.some((finding) => finding.classification === 'blocking')) {
|
|
320
|
+
const receipt = createReceipt({
|
|
321
|
+
kind: 'Runtime Application',
|
|
322
|
+
operation: 'Runtime Application',
|
|
323
|
+
scope,
|
|
324
|
+
trigger: 'reload projected Effective State',
|
|
325
|
+
expectedStateRevision: stateRevision,
|
|
326
|
+
durableOutcome: 'unchanged',
|
|
327
|
+
runtimeOutcome: 'pending-application',
|
|
328
|
+
summary: 'Pending Application',
|
|
329
|
+
findings: input.wholePluginFindings,
|
|
330
|
+
recoversReceiptId: input.recoversReceiptId,
|
|
331
|
+
});
|
|
332
|
+
await appendReceipt(scope, receipt, opts);
|
|
333
|
+
return {
|
|
334
|
+
outcome: 'pending-application',
|
|
335
|
+
receipt,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const applied = await verifyReload();
|
|
340
|
+
const receipt = createReceipt({
|
|
341
|
+
kind: 'Runtime Application',
|
|
342
|
+
operation: 'Runtime Application',
|
|
343
|
+
scope,
|
|
344
|
+
trigger: 'reload projected Effective State',
|
|
345
|
+
expectedStateRevision: stateRevision,
|
|
346
|
+
observedStateRevision: applied ? stateRevision : undefined,
|
|
347
|
+
durableOutcome: 'unchanged',
|
|
348
|
+
runtimeOutcome: applied ? 'applied' : 'pending-application',
|
|
349
|
+
summary: applied ? 'Completed' : 'Pending Application',
|
|
350
|
+
recoversReceiptId: input.recoversReceiptId,
|
|
351
|
+
});
|
|
352
|
+
await appendReceipt(scope, receipt, opts);
|
|
353
|
+
|
|
354
|
+
return {
|
|
355
|
+
outcome: applied ? 'applied' : 'pending-application',
|
|
356
|
+
receipt,
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|