@hobin/developer 0.1.7 → 0.1.8
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/README.md +17 -3
- package/extensions/developer.ts +68 -5
- package/extensions/tool-policy.ts +87 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -74,6 +74,14 @@ workspace boundary. Developer recognizes Pi built-ins from their provenance,
|
|
|
74
74
|
preserves unrelated active tools, and does not force-enable tools the user
|
|
75
75
|
disabled.
|
|
76
76
|
|
|
77
|
+
Before Pi tears down a session runtime, Developer releases its route-bound tool
|
|
78
|
+
delta so a reload, session replacement, or later extension instance does not
|
|
79
|
+
inherit orphaned restrictions. After that release, the package records a
|
|
80
|
+
reload-safe lifecycle marker. If an in-process reload finds older Developer
|
|
81
|
+
history without a later release marker, Developer stays off and requires a full
|
|
82
|
+
Pi process restart. Neither `/reload` nor `/develop off` → `on` can safely infer
|
|
83
|
+
which built-ins the user originally enabled.
|
|
84
|
+
|
|
77
85
|
## How a request runs
|
|
78
86
|
|
|
79
87
|
Developer adds two model-facing protocol tools:
|
|
@@ -300,6 +308,10 @@ eligibility are derived from the same machine snapshot.
|
|
|
300
308
|
The current event contract is `developer/v5`. Earlier protocol entries are not
|
|
301
309
|
replayed; this breaking contract uses `target`, `implementation`, and
|
|
302
310
|
`before-implementation` consistently across tools, events, and machine state.
|
|
311
|
+
A process started on the current package may open a branch containing those
|
|
312
|
+
entries, but hot-reloading directly from a pre-handoff runtime is rejected with
|
|
313
|
+
a restart instruction so its in-memory tool ownership cannot be mistaken for
|
|
314
|
+
user configuration.
|
|
303
315
|
|
|
304
316
|
Developer deliberately leaves compaction ownership to Pi. It does not trigger,
|
|
305
317
|
cancel, or replace threshold/overflow compaction, so it cannot override the
|
|
@@ -402,9 +414,11 @@ not append entries, submit answers, register tools, mutate active tools, restore
|
|
|
402
414
|
sessions, start network work, or send model messages. Prepared answers are shown
|
|
403
415
|
only as a character count.
|
|
404
416
|
|
|
405
|
-
`check` validates package structure and deterministic behavior
|
|
406
|
-
|
|
407
|
-
|
|
417
|
+
`check` validates package structure and deterministic behavior, including
|
|
418
|
+
reload migration detection and shutdown restoration of Developer-owned tool
|
|
419
|
+
deltas. `eval` launches the real Pi RPC surface without a model and covers
|
|
420
|
+
package resources, commands, activation state, and route-bound tool gating.
|
|
421
|
+
These are release-gating checks.
|
|
408
422
|
|
|
409
423
|
Model-dependent runs are probabilistic evaluations rather than binary tests:
|
|
410
424
|
|
package/extensions/developer.ts
CHANGED
|
@@ -53,9 +53,12 @@ import {
|
|
|
53
53
|
type RouteEvent,
|
|
54
54
|
} from "./state.ts";
|
|
55
55
|
import {
|
|
56
|
+
TOOL_POLICY_LIFECYCLE_ENTRY,
|
|
56
57
|
builtinControlledToolCapabilities,
|
|
57
58
|
isControlledToolAllowed,
|
|
58
59
|
reconcileProtocolTools,
|
|
60
|
+
reloadSafeToolPolicyMarker,
|
|
61
|
+
toolPolicyReloadRequiresRestart,
|
|
59
62
|
type ProtocolToolAccess,
|
|
60
63
|
type ToolPolicyMemory,
|
|
61
64
|
} from "./tool-policy.ts";
|
|
@@ -88,6 +91,8 @@ const MAX_EVIDENCE_CHARS = 2_000;
|
|
|
88
91
|
const MAX_RESULT_CHARS = 12_000;
|
|
89
92
|
const MAX_ARTIFACT_CHARS = 4_096;
|
|
90
93
|
const DEVELOPER_COMMAND_ACTIONS = ["on", "status", "questions", "off"] as const;
|
|
94
|
+
const TOOL_POLICY_RESTART_MESSAGE =
|
|
95
|
+
"Developer detected an in-process package reload from a version without reload-safe tool handoff. Restart the Pi process before enabling Developer; /reload and /develop off/on cannot safely reconstruct the prior built-in tool selection.";
|
|
91
96
|
|
|
92
97
|
function textResult<T>(text: string, details: T) {
|
|
93
98
|
return {
|
|
@@ -560,6 +565,7 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
560
565
|
let routeOpening = false;
|
|
561
566
|
const routesWithMutation = new Set<string>();
|
|
562
567
|
let toolPolicyMemory: ToolPolicyMemory = { withheldBuiltins: new Set() };
|
|
568
|
+
let toolPolicyRestartRequired = false;
|
|
563
569
|
|
|
564
570
|
pi.registerFlag("develop", {
|
|
565
571
|
description: "Start with the Developer protocol enabled",
|
|
@@ -582,7 +588,31 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
582
588
|
pi.setActiveTools(next.activeTools);
|
|
583
589
|
};
|
|
584
590
|
|
|
591
|
+
const releaseProtocolTools = () => {
|
|
592
|
+
const current = pi.getActiveTools();
|
|
593
|
+
const next = reconcileProtocolTools({
|
|
594
|
+
activeTools: current,
|
|
595
|
+
allTools: pi.getAllTools(),
|
|
596
|
+
enabled: false,
|
|
597
|
+
access: protocolToolAccess(state),
|
|
598
|
+
protocolTools: PROTOCOL_TOOLS,
|
|
599
|
+
memory: toolPolicyMemory,
|
|
600
|
+
});
|
|
601
|
+
toolPolicyMemory = next.memory;
|
|
602
|
+
if (!sameToolSet(current, next.activeTools))
|
|
603
|
+
pi.setActiveTools(next.activeTools);
|
|
604
|
+
};
|
|
605
|
+
|
|
585
606
|
const refreshUI = (ctx: ExtensionContext) => {
|
|
607
|
+
if (toolPolicyRestartRequired) {
|
|
608
|
+
ctx.ui.setStatus("developer", "developer · restart required");
|
|
609
|
+
ctx.ui.setWidget(
|
|
610
|
+
"developer",
|
|
611
|
+
["blocked · restart Pi to reset Developer tool access"],
|
|
612
|
+
{ placement: "belowEditor" },
|
|
613
|
+
);
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
586
616
|
if (!state.enabled) {
|
|
587
617
|
ctx.ui.setStatus("developer", undefined);
|
|
588
618
|
ctx.ui.setWidget("developer", undefined);
|
|
@@ -643,12 +673,19 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
643
673
|
};
|
|
644
674
|
|
|
645
675
|
const reconstruct = (ctx: ExtensionContext) => {
|
|
646
|
-
state =
|
|
676
|
+
state = toolPolicyRestartRequired
|
|
677
|
+
? initialState()
|
|
678
|
+
: reconstructState(ctx.sessionManager.getBranch());
|
|
647
679
|
syncProtocolTools();
|
|
648
680
|
refreshUI(ctx);
|
|
649
681
|
};
|
|
650
682
|
|
|
651
|
-
const setEnabled = (enabled: boolean, ctx: ExtensionContext) => {
|
|
683
|
+
const setEnabled = (enabled: boolean, ctx: ExtensionContext): boolean => {
|
|
684
|
+
if (enabled && toolPolicyRestartRequired) {
|
|
685
|
+
ctx.ui.notify(TOOL_POLICY_RESTART_MESSAGE, "error");
|
|
686
|
+
refreshUI(ctx);
|
|
687
|
+
return false;
|
|
688
|
+
}
|
|
652
689
|
const event: ActivationEvent = {
|
|
653
690
|
protocol: PROTOCOL,
|
|
654
691
|
kind: "activation",
|
|
@@ -658,6 +695,7 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
658
695
|
state = applyDeveloperEvent(state, event);
|
|
659
696
|
syncProtocolTools();
|
|
660
697
|
refreshUI(ctx);
|
|
698
|
+
return true;
|
|
661
699
|
};
|
|
662
700
|
|
|
663
701
|
const SharedRouteParams = {
|
|
@@ -1576,9 +1614,10 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
1576
1614
|
return true;
|
|
1577
1615
|
};
|
|
1578
1616
|
|
|
1579
|
-
const setAndNotifyEnabled = (enabled: boolean) => {
|
|
1580
|
-
setEnabled(enabled, ctx);
|
|
1617
|
+
const setAndNotifyEnabled = (enabled: boolean): boolean => {
|
|
1618
|
+
if (!setEnabled(enabled, ctx)) return false;
|
|
1581
1619
|
ctx.ui.notify(`Developer: ${enabled ? "on" : "off"}`, "info");
|
|
1620
|
+
return true;
|
|
1582
1621
|
};
|
|
1583
1622
|
|
|
1584
1623
|
const turnOff = async (): Promise<boolean> => {
|
|
@@ -1603,6 +1642,11 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
1603
1642
|
};
|
|
1604
1643
|
|
|
1605
1644
|
const inspectStatus = async () => {
|
|
1645
|
+
if (toolPolicyRestartRequired) {
|
|
1646
|
+
ctx.ui.notify(TOOL_POLICY_RESTART_MESSAGE, "error");
|
|
1647
|
+
refreshUI(ctx);
|
|
1648
|
+
return;
|
|
1649
|
+
}
|
|
1606
1650
|
refreshAvailableSkills(ctx);
|
|
1607
1651
|
if (ctx.mode === "tui") {
|
|
1608
1652
|
await showDeveloperStatus(ctx, {
|
|
@@ -1796,14 +1840,33 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
1796
1840
|
};
|
|
1797
1841
|
});
|
|
1798
1842
|
|
|
1799
|
-
pi.on("session_start", (
|
|
1843
|
+
pi.on("session_start", (event, ctx) => {
|
|
1844
|
+
const branch = ctx.sessionManager.getBranch();
|
|
1845
|
+
toolPolicyRestartRequired =
|
|
1846
|
+
event.reason === "reload" &&
|
|
1847
|
+
toolPolicyReloadRequiresRestart({
|
|
1848
|
+
entries: branch,
|
|
1849
|
+
protocol: PROTOCOL,
|
|
1850
|
+
protocolTools: PROTOCOL_TOOLS,
|
|
1851
|
+
});
|
|
1800
1852
|
reconstruct(ctx);
|
|
1853
|
+
if (toolPolicyRestartRequired) {
|
|
1854
|
+
ctx.ui.notify(TOOL_POLICY_RESTART_MESSAGE, "error");
|
|
1855
|
+
return;
|
|
1856
|
+
}
|
|
1801
1857
|
const startEnabled = pi.getFlag("develop");
|
|
1802
1858
|
if (startEnabled === true && !state.enabled) setEnabled(true, ctx);
|
|
1803
1859
|
});
|
|
1804
1860
|
pi.on("session_tree", (_event, ctx) => reconstruct(ctx));
|
|
1805
1861
|
pi.on("agent_settled", (_event, ctx) => refreshUI(ctx));
|
|
1806
1862
|
pi.on("session_shutdown", (_event, ctx) => {
|
|
1863
|
+
releaseProtocolTools();
|
|
1864
|
+
if (!toolPolicyRestartRequired) {
|
|
1865
|
+
pi.appendEntry(
|
|
1866
|
+
TOOL_POLICY_LIFECYCLE_ENTRY,
|
|
1867
|
+
reloadSafeToolPolicyMarker(PROTOCOL),
|
|
1868
|
+
);
|
|
1869
|
+
}
|
|
1807
1870
|
ctx.ui.setStatus("developer", undefined);
|
|
1808
1871
|
ctx.ui.setWidget("developer", undefined);
|
|
1809
1872
|
});
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
export type ControlledToolCapability = "shell" | "artifact";
|
|
2
2
|
|
|
3
|
+
export const TOOL_POLICY_LIFECYCLE_ENTRY = "developer.tool-policy-lifecycle";
|
|
4
|
+
export const TOOL_POLICY_LIFECYCLE = "released-before-reload/v1";
|
|
5
|
+
|
|
3
6
|
const CONTROLLED_BUILTIN_CAPABILITIES = new Map<
|
|
4
7
|
string,
|
|
5
8
|
ControlledToolCapability
|
|
@@ -29,6 +32,90 @@ export interface ProtocolToolAccess {
|
|
|
29
32
|
hasBeforeImplementationGate: boolean;
|
|
30
33
|
}
|
|
31
34
|
|
|
35
|
+
interface ToolPolicyBranchEntry {
|
|
36
|
+
type?: string;
|
|
37
|
+
customType?: string;
|
|
38
|
+
data?: unknown;
|
|
39
|
+
message?: {
|
|
40
|
+
role?: string;
|
|
41
|
+
toolName?: string;
|
|
42
|
+
details?: unknown;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface ToolPolicyLifecycleMarker {
|
|
47
|
+
protocol: string;
|
|
48
|
+
kind: "tool-policy-lifecycle";
|
|
49
|
+
lifecycle: typeof TOOL_POLICY_LIFECYCLE;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function isObject(value: unknown): value is Record<string, unknown> {
|
|
53
|
+
return typeof value === "object" && value !== null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function reloadSafeToolPolicyMarker(
|
|
57
|
+
protocol: string,
|
|
58
|
+
): ToolPolicyLifecycleMarker {
|
|
59
|
+
return {
|
|
60
|
+
protocol,
|
|
61
|
+
kind: "tool-policy-lifecycle",
|
|
62
|
+
lifecycle: TOOL_POLICY_LIFECYCLE,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function isReloadSafeToolPolicyMarker(
|
|
67
|
+
entry: ToolPolicyBranchEntry,
|
|
68
|
+
protocol: string,
|
|
69
|
+
): boolean {
|
|
70
|
+
if (
|
|
71
|
+
entry.type !== "custom" ||
|
|
72
|
+
entry.customType !== TOOL_POLICY_LIFECYCLE_ENTRY ||
|
|
73
|
+
!isObject(entry.data)
|
|
74
|
+
)
|
|
75
|
+
return false;
|
|
76
|
+
return (
|
|
77
|
+
entry.data.protocol === protocol &&
|
|
78
|
+
entry.data.kind === "tool-policy-lifecycle" &&
|
|
79
|
+
entry.data.lifecycle === TOOL_POLICY_LIFECYCLE
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function toolPolicyReloadRequiresRestart(input: {
|
|
84
|
+
entries: readonly ToolPolicyBranchEntry[];
|
|
85
|
+
protocol: string;
|
|
86
|
+
protocolTools: readonly string[];
|
|
87
|
+
}): boolean {
|
|
88
|
+
const protocolTools = new Set(input.protocolTools);
|
|
89
|
+
for (let index = input.entries.length - 1; index >= 0; index -= 1) {
|
|
90
|
+
const entry = input.entries[index];
|
|
91
|
+
if (!entry) continue;
|
|
92
|
+
if (isReloadSafeToolPolicyMarker(entry, input.protocol)) return false;
|
|
93
|
+
|
|
94
|
+
let value: unknown;
|
|
95
|
+
if (
|
|
96
|
+
entry.type === "custom" &&
|
|
97
|
+
entry.customType?.startsWith("developer.") &&
|
|
98
|
+
entry.customType !== TOOL_POLICY_LIFECYCLE_ENTRY
|
|
99
|
+
) {
|
|
100
|
+
value = entry.data;
|
|
101
|
+
} else if (
|
|
102
|
+
entry.type === "message" &&
|
|
103
|
+
entry.message?.role === "toolResult" &&
|
|
104
|
+
entry.message.toolName &&
|
|
105
|
+
protocolTools.has(entry.message.toolName)
|
|
106
|
+
) {
|
|
107
|
+
value = entry.message.details;
|
|
108
|
+
}
|
|
109
|
+
if (
|
|
110
|
+
isObject(value) &&
|
|
111
|
+
typeof value.protocol === "string" &&
|
|
112
|
+
value.protocol.startsWith("developer/")
|
|
113
|
+
)
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
|
|
32
119
|
export function builtinControlledToolCapabilities(
|
|
33
120
|
tools: ToolMetadataLike[],
|
|
34
121
|
): Map<string, ControlledToolCapability> {
|