@dsh-cc/subagent-resume-pins 0.5.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 +201 -0
- package/README.md +48 -0
- package/lib/fingerprint.d.ts +25 -0
- package/lib/fingerprint.d.ts.map +1 -0
- package/lib/fingerprint.js +49 -0
- package/lib/fingerprint.js.map +1 -0
- package/lib/gate.d.ts +98 -0
- package/lib/gate.d.ts.map +1 -0
- package/lib/gate.js +218 -0
- package/lib/gate.js.map +1 -0
- package/lib/index.d.ts +22 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +22 -0
- package/lib/index.js.map +1 -0
- package/lib/overlay.d.ts +37 -0
- package/lib/overlay.d.ts.map +1 -0
- package/lib/overlay.js +80 -0
- package/lib/overlay.js.map +1 -0
- package/lib/pin.d.ts +129 -0
- package/lib/pin.d.ts.map +1 -0
- package/lib/pin.js +211 -0
- package/lib/pin.js.map +1 -0
- package/lib/plugin.d.ts +67 -0
- package/lib/plugin.d.ts.map +1 -0
- package/lib/plugin.js +354 -0
- package/lib/plugin.js.map +1 -0
- package/lib/policy.d.ts +41 -0
- package/lib/policy.d.ts.map +1 -0
- package/lib/policy.js +45 -0
- package/lib/policy.js.map +1 -0
- package/lib/serialize.d.ts +27 -0
- package/lib/serialize.d.ts.map +1 -0
- package/lib/serialize.js +38 -0
- package/lib/serialize.js.map +1 -0
- package/lib/store.d.ts +62 -0
- package/lib/store.d.ts.map +1 -0
- package/lib/store.js +133 -0
- package/lib/store.js.map +1 -0
- package/package.json +78 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work
|
|
38
|
+
(an example is provided in the Appendix below).
|
|
39
|
+
|
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
46
|
+
the Work and Derivative Works thereof.
|
|
47
|
+
|
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
|
49
|
+
the original version of the Work and any modifications or additions
|
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
65
|
+
|
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
|
72
|
+
|
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
78
|
+
where such license applies only to those patent claims licensable
|
|
79
|
+
by such Contributor that are necessarily infringed by their
|
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
82
|
+
institute patent litigation against any entity (including a
|
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
+
or contributory patent infringement, then any patent licenses
|
|
86
|
+
granted to You under this License for that Work shall terminate
|
|
87
|
+
as of the date such litigation is filed.
|
|
88
|
+
|
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
91
|
+
modifications, and in Source or Object form, provided that You
|
|
92
|
+
meet the following conditions:
|
|
93
|
+
|
|
94
|
+
(a) You must give any other recipients of the Work or
|
|
95
|
+
Derivative Works a copy of this License; and
|
|
96
|
+
|
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
|
98
|
+
stating that You changed the files; and
|
|
99
|
+
|
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
|
102
|
+
attribution notices from the Source form of the Work,
|
|
103
|
+
excluding those notices that do not pertain to any part of
|
|
104
|
+
the Derivative Works; and
|
|
105
|
+
|
|
106
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
|
108
|
+
include a readable copy of the attribution notices contained
|
|
109
|
+
within such NOTICE file, excluding those notices that do not
|
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
|
111
|
+
of the following places: within a NOTICE text file distributed
|
|
112
|
+
as part of the Derivative Works; within the Source form or
|
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
|
114
|
+
within a display generated by the Derivative Works, if and
|
|
115
|
+
wherever such third-party notices normally appear. The contents
|
|
116
|
+
of the NOTICE file are for informational purposes only and
|
|
117
|
+
do not modify the License. You may add Your own attribution
|
|
118
|
+
notices within Derivative Works that You distribute, alongside
|
|
119
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
120
|
+
that such additional attribution notices cannot be construed
|
|
121
|
+
as modifying the License.
|
|
122
|
+
|
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
|
124
|
+
may provide additional or different license terms and conditions
|
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
128
|
+
the conditions stated in this License.
|
|
129
|
+
|
|
130
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
131
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
132
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
133
|
+
this License, without any additional terms or conditions.
|
|
134
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
135
|
+
the terms of any separate license agreement you may have executed
|
|
136
|
+
with Licensor regarding such Contributions.
|
|
137
|
+
|
|
138
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
139
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
140
|
+
except as required for reasonable and customary use in describing the
|
|
141
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
142
|
+
|
|
143
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
144
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
145
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
146
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
147
|
+
implied, including, without limitation, any warranties or conditions
|
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
150
|
+
appropriateness of using or redistributing the Work and assume any
|
|
151
|
+
risks associated with Your exercise of permissions under this License.
|
|
152
|
+
|
|
153
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
154
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
155
|
+
unless required by applicable law (such as deliberate and grossly
|
|
156
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
157
|
+
liable to You for damages, including any direct, indirect, special,
|
|
158
|
+
incidental, or consequential damages of any character arising as a
|
|
159
|
+
result of this License or out of the use or inability to use the
|
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
162
|
+
other commercial damages or losses), even if such Contributor
|
|
163
|
+
has been advised of the possibility of such damages.
|
|
164
|
+
|
|
165
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
166
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
167
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
168
|
+
or other liability obligations and/or rights consistent with this
|
|
169
|
+
License. However, in accepting such obligations, You may act only
|
|
170
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
171
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
172
|
+
defend, and hold each Contributor harmless for any liability
|
|
173
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
174
|
+
of your accepting any such warranty or additional liability.
|
|
175
|
+
|
|
176
|
+
END OF TERMS AND CONDITIONS
|
|
177
|
+
|
|
178
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
179
|
+
|
|
180
|
+
To apply the Apache License to your work, attach the following
|
|
181
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
182
|
+
replaced with your own identifying information. (Don't include
|
|
183
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
184
|
+
comment syntax for the file format. We also recommend that a
|
|
185
|
+
file or class name and description of purpose be included on the
|
|
186
|
+
same "printed page" as the copyright notice for easier
|
|
187
|
+
identification within third-party archives.
|
|
188
|
+
|
|
189
|
+
Copyright [yyyy] [name of copyright owner]
|
|
190
|
+
|
|
191
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
192
|
+
you may not use this file except in compliance with the License.
|
|
193
|
+
You may obtain a copy of the License at
|
|
194
|
+
|
|
195
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
196
|
+
|
|
197
|
+
Unless required by applicable law or agreed to in writing, software
|
|
198
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
199
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
200
|
+
See the License for the specific language governing permissions and
|
|
201
|
+
limitations under the License.
|
package/README.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# @dsh-cc/subagent-resume-pins
|
|
2
|
+
|
|
3
|
+
Pinned resume descriptors ("resume pins") for continuable background subagents.
|
|
4
|
+
Design record: `docs/plans/2026-09-04-subagent-resume-pins.md`.
|
|
5
|
+
|
|
6
|
+
A cold resume of a background child (parent session disposed, then a new Context
|
|
7
|
+
boots over the same persistence root and `send_message` addresses the child)
|
|
8
|
+
restores the harness descriptor's headers — persona, tool filter, model route —
|
|
9
|
+
but drops every other spawn-time `AgentOptions` field, notably the alias-stamped
|
|
10
|
+
`reasoningEffort` and `maxTokens`. This package closes that gap: the spawn path
|
|
11
|
+
pins the child's effective runtime config to disk, and the resume path re-applies
|
|
12
|
+
it — visibly, never silently.
|
|
13
|
+
|
|
14
|
+
## What it mounts
|
|
15
|
+
|
|
16
|
+
One cordis plugin (`apply`) with:
|
|
17
|
+
|
|
18
|
+
- a **`PinStore`** (atomic per-child `<childId>.json` files under `pinsRoot`),
|
|
19
|
+
published as the `resumePinStore` service so the Task plugin's spawn capture
|
|
20
|
+
shares one cache with the gate and overlay;
|
|
21
|
+
- a **`tools/pre-execute` resume gate** on `send_message` to a pinned child with
|
|
22
|
+
no live Activation: session existence (`PIN_ORPHANED`), pin readability
|
|
23
|
+
(`PIN_UNREADABLE`), workspace existence/identity (`WORKSPACE_MISSING`,
|
|
24
|
+
`WORKSPACE_CHANGED`), definition re-fingerprint (`DEFINITION_CHANGED`), pinned
|
|
25
|
+
tool availability (`PINNED_TOOL_UNAVAILABLE`), and pinned-route availability
|
|
26
|
+
(`SUBAGENT_MODEL_UNAVAILABLE`) — every deny persisted into the pin
|
|
27
|
+
(`resume.state='blocked'`) **before** the deny returns;
|
|
28
|
+
- an **`agent/request` overlay** applying the pinned
|
|
29
|
+
`{provider, model, reasoningEffort, maxTokens}` tuple field-by-field
|
|
30
|
+
(absence included) to every resumed turn, whatever resumed it;
|
|
31
|
+
- **`tools/post-execute`** notice prefixing on `send_message` and
|
|
32
|
+
`resumeState`/`definitionChanged` annotation on `list_agents`;
|
|
33
|
+
- the **`subagents-resume` settings namespace** (kebab-case) with the policy
|
|
34
|
+
knobs `onUnavailableModel`, `onDefinitionChanged`, `onWorkspaceChanged`
|
|
35
|
+
(`resume-with-notice` defaults, `block` opt-in, `route-current` fallback for
|
|
36
|
+
the model route; the always-block conditions have no safe fallback).
|
|
37
|
+
|
|
38
|
+
Zero-op when unmounted: pins are simply unread and behavior is the legacy
|
|
39
|
+
behavior. Only pinned children are affected; a missing pin is a legacy/foreign
|
|
40
|
+
child (pass-through) and same-epoch followups to a live Activation are untouched.
|
|
41
|
+
|
|
42
|
+
## Composition
|
|
43
|
+
|
|
44
|
+
The cc preset mounts the row (`cc-resume-pins`, inside the `cc-services` isolate
|
|
45
|
+
group) with `pinsRoot: !!js dshHomePath('sessions', 'resume-pins')` — colocated
|
|
46
|
+
with the harness base patch's jsonl session-persistence root — **before**
|
|
47
|
+
`tool-task`, whose spawn capture prefers the shared service store. Standalone
|
|
48
|
+
consumers may instead pass `resumePins: { pinsRoot }` to the Task plugin config.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Definition fingerprints (§4.4): a named agent definition is fingerprinted by
|
|
3
|
+
* hashing the canonical JSON of all recognized parsed frontmatter fields plus
|
|
4
|
+
* the persona body. The rule is parse-level canonicalization — comment and
|
|
5
|
+
* formatting-only markdown edits produce no fingerprint change, while a change
|
|
6
|
+
* to ANY declared field (consumed like `model`/`tools`/persona, or currently
|
|
7
|
+
* inert like `maxTurns`/`effort`) produces one. Discovery metadata
|
|
8
|
+
* (`source`, `baseDir`, `filename`) is deliberately excluded: the same file
|
|
9
|
+
* discovered from a different layer must keep its identity.
|
|
10
|
+
*
|
|
11
|
+
* @module @dsh-cc/subagent-resume-pins/fingerprint
|
|
12
|
+
*/
|
|
13
|
+
import type { AgentDefinition } from '@dsh-cc/claude-code-agents';
|
|
14
|
+
/** sha256 hex of `input`, prefixed `sha256:`. */
|
|
15
|
+
export declare function sha256Prefixed(input: string): string;
|
|
16
|
+
/** Stable hash of a persona (system-prompt) string. */
|
|
17
|
+
export declare function personaHash(persona: string): string;
|
|
18
|
+
/**
|
|
19
|
+
* Fingerprint one parsed {@link AgentDefinition} over its content fields:
|
|
20
|
+
* every recognized parsed frontmatter field plus the persona body, in
|
|
21
|
+
* stable key order. Optional fields absent from the definition are simply
|
|
22
|
+
* omitted from the canonical form.
|
|
23
|
+
*/
|
|
24
|
+
export declare function definitionFingerprint(def: AgentDefinition): string;
|
|
25
|
+
//# sourceMappingURL=fingerprint.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fingerprint.d.ts","sourceRoot":"","sources":["../src/fingerprint.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAA;AAEjE,iDAAiD;AACjD,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEpD;AAED,uDAAuD;AACvD,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,eAAe,GAAG,MAAM,CAmBlE"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Definition fingerprints (§4.4): a named agent definition is fingerprinted by
|
|
3
|
+
* hashing the canonical JSON of all recognized parsed frontmatter fields plus
|
|
4
|
+
* the persona body. The rule is parse-level canonicalization — comment and
|
|
5
|
+
* formatting-only markdown edits produce no fingerprint change, while a change
|
|
6
|
+
* to ANY declared field (consumed like `model`/`tools`/persona, or currently
|
|
7
|
+
* inert like `maxTurns`/`effort`) produces one. Discovery metadata
|
|
8
|
+
* (`source`, `baseDir`, `filename`) is deliberately excluded: the same file
|
|
9
|
+
* discovered from a different layer must keep its identity.
|
|
10
|
+
*
|
|
11
|
+
* @module @dsh-cc/subagent-resume-pins/fingerprint
|
|
12
|
+
*/
|
|
13
|
+
import { createHash } from 'node:crypto';
|
|
14
|
+
import { canonicalJson } from "./pin.js";
|
|
15
|
+
/** sha256 hex of `input`, prefixed `sha256:`. */
|
|
16
|
+
export function sha256Prefixed(input) {
|
|
17
|
+
return `sha256:${createHash('sha256').update(input, 'utf8').digest('hex')}`;
|
|
18
|
+
}
|
|
19
|
+
/** Stable hash of a persona (system-prompt) string. */
|
|
20
|
+
export function personaHash(persona) {
|
|
21
|
+
return sha256Prefixed(persona);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Fingerprint one parsed {@link AgentDefinition} over its content fields:
|
|
25
|
+
* every recognized parsed frontmatter field plus the persona body, in
|
|
26
|
+
* stable key order. Optional fields absent from the definition are simply
|
|
27
|
+
* omitted from the canonical form.
|
|
28
|
+
*/
|
|
29
|
+
export function definitionFingerprint(def) {
|
|
30
|
+
const content = {
|
|
31
|
+
agentType: def.agentType,
|
|
32
|
+
whenToUse: def.whenToUse,
|
|
33
|
+
systemPrompt: def.systemPrompt,
|
|
34
|
+
toolRestriction: def.toolRestriction ?? null,
|
|
35
|
+
skills: def.skills ?? null,
|
|
36
|
+
mcpServers: def.mcpServers ?? null,
|
|
37
|
+
hooks: def.hooks ?? null,
|
|
38
|
+
model: def.model ?? null,
|
|
39
|
+
effort: def.effort ?? null,
|
|
40
|
+
permissionMode: def.permissionMode ?? null,
|
|
41
|
+
maxTurns: def.maxTurns ?? null,
|
|
42
|
+
initialPrompt: def.initialPrompt ?? null,
|
|
43
|
+
background: def.background ?? null,
|
|
44
|
+
memory: def.memory ?? null,
|
|
45
|
+
isolation: def.isolation ?? null,
|
|
46
|
+
};
|
|
47
|
+
return sha256Prefixed(canonicalJson(content));
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=fingerprint.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fingerprint.js","sourceRoot":"","sources":["../src/fingerprint.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AACxC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAGxC,iDAAiD;AACjD,MAAM,UAAU,cAAc,CAAC,KAAa;IAC1C,OAAO,UAAU,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAA;AAC7E,CAAC;AAED,uDAAuD;AACvD,MAAM,UAAU,WAAW,CAAC,OAAe;IACzC,OAAO,cAAc,CAAC,OAAO,CAAC,CAAA;AAChC,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,qBAAqB,CAAC,GAAoB;IACxD,MAAM,OAAO,GAAG;QACd,SAAS,EAAE,GAAG,CAAC,SAAS;QACxB,SAAS,EAAE,GAAG,CAAC,SAAS;QACxB,YAAY,EAAE,GAAG,CAAC,YAAY;QAC9B,eAAe,EAAE,GAAG,CAAC,eAAe,IAAI,IAAI;QAC5C,MAAM,EAAE,GAAG,CAAC,MAAM,IAAI,IAAI;QAC1B,UAAU,EAAE,GAAG,CAAC,UAAU,IAAI,IAAI;QAClC,KAAK,EAAE,GAAG,CAAC,KAAK,IAAI,IAAI;QACxB,KAAK,EAAE,GAAG,CAAC,KAAK,IAAI,IAAI;QACxB,MAAM,EAAE,GAAG,CAAC,MAAM,IAAI,IAAI;QAC1B,cAAc,EAAE,GAAG,CAAC,cAAc,IAAI,IAAI;QAC1C,QAAQ,EAAE,GAAG,CAAC,QAAQ,IAAI,IAAI;QAC9B,aAAa,EAAE,GAAG,CAAC,aAAa,IAAI,IAAI;QACxC,UAAU,EAAE,GAAG,CAAC,UAAU,IAAI,IAAI;QAClC,MAAM,EAAE,GAAG,CAAC,MAAM,IAAI,IAAI;QAC1B,SAAS,EAAE,GAAG,CAAC,SAAS,IAAI,IAAI;KACjC,CAAA;IACD,OAAO,cAAc,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,CAAA;AAC/C,CAAC"}
|
package/lib/gate.d.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The PURE resume gate (plan §4.6): re-evaluates one pin against current
|
|
3
|
+
* environment facts on every cold resume. No cordis, no IO of its own — the
|
|
4
|
+
* environment supplies already-probed facts and throwing availability
|
|
5
|
+
* resolvers; the caller (the plugin) persists any deny to the pin BEFORE the
|
|
6
|
+
* decision is returned (durability ordering) and is also responsible for
|
|
7
|
+
* skipping the gate entirely when no pin exists (legacy/foreign child).
|
|
8
|
+
*
|
|
9
|
+
* A persisted `resume.state='blocked'` never short-circuits: policy flips and
|
|
10
|
+
* recovered conditions are authoritative, the stored state is derived.
|
|
11
|
+
*
|
|
12
|
+
* @module @dsh-cc/subagent-resume-pins/gate
|
|
13
|
+
*/
|
|
14
|
+
import type { CorruptPin } from './store.ts';
|
|
15
|
+
import type { OverlayTuple, ResumePin } from './pin.ts';
|
|
16
|
+
import type { ResumePolicy } from './policy.ts';
|
|
17
|
+
/** Why a cold resume was denied. The first five always block (no fallback). */
|
|
18
|
+
export type DenyCode = 'PIN_ORPHANED' | 'PIN_UNREADABLE' | 'WORKSPACE_MISSING' | 'PINNED_TOOL_UNAVAILABLE' | 'SUBAGENT_MODEL_UNAVAILABLE' | 'WORKSPACE_CHANGED' | 'DEFINITION_CHANGED' | 'STORE_WRITE_FAILURE';
|
|
19
|
+
/** A resolved call config as the availability preflight returns it. */
|
|
20
|
+
export interface GateResolvedConfig {
|
|
21
|
+
readonly provider: string;
|
|
22
|
+
readonly model: string;
|
|
23
|
+
readonly reasoningEffort?: string | undefined;
|
|
24
|
+
readonly maxTokens?: number | undefined;
|
|
25
|
+
}
|
|
26
|
+
/** The route fields a selector resolution may contribute. */
|
|
27
|
+
export interface GateDetailedRoute {
|
|
28
|
+
readonly via: 'alias' | 'literal' | 'inherit';
|
|
29
|
+
readonly route: {
|
|
30
|
+
readonly provider?: string;
|
|
31
|
+
readonly model?: string;
|
|
32
|
+
readonly reasoningEffort?: string;
|
|
33
|
+
} | undefined;
|
|
34
|
+
}
|
|
35
|
+
/** The probed environment facts + availability resolvers one evaluation uses. */
|
|
36
|
+
export interface GateEnv {
|
|
37
|
+
/** The child has a persisted session (step 0: false → orphaned pin). */
|
|
38
|
+
readonly sessionExists: boolean;
|
|
39
|
+
/** The pinned workspace cwd still exists on disk. */
|
|
40
|
+
readonly cwdExists: boolean;
|
|
41
|
+
/** The CURRENT git identity of the workspace cwd (probe already run). */
|
|
42
|
+
readonly currentGit: {
|
|
43
|
+
readonly gitDir: string;
|
|
44
|
+
readonly gitCommonDir: string;
|
|
45
|
+
readonly branch: string;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Re-fingerprinted definition identity (step 3, named pins only): a current
|
|
49
|
+
* fingerprint string, `'missing'` for a gone/unreadable definition, or
|
|
50
|
+
* `null` when no current information exists (no current information check).
|
|
51
|
+
*/
|
|
52
|
+
readonly currentDefinitionFingerprint: string | 'missing' | null;
|
|
53
|
+
/**
|
|
54
|
+
* The calling parent agent's CURRENT route ({@link AgentOptions} subset).
|
|
55
|
+
* The route-current fallback overlays a freshly-resolved selector onto THIS
|
|
56
|
+
* route — never onto the pinned tuple — so parent-route drift is honored.
|
|
57
|
+
*/
|
|
58
|
+
readonly currentRoute?: {
|
|
59
|
+
readonly provider?: string;
|
|
60
|
+
readonly model?: string;
|
|
61
|
+
readonly maxTokens?: number;
|
|
62
|
+
};
|
|
63
|
+
/** The current restrictable tool-name universe (step 4). */
|
|
64
|
+
readonly restrictableNames: ReadonlySet<string>;
|
|
65
|
+
/**
|
|
66
|
+
* §4.3 preflight against the LIVE registrations; throws when the provider
|
|
67
|
+
* is unmounted, the route invalid, or a requested control unsupported.
|
|
68
|
+
*/
|
|
69
|
+
resolveCallConfig(config: {
|
|
70
|
+
provider: string;
|
|
71
|
+
model: string;
|
|
72
|
+
maxTokens?: number;
|
|
73
|
+
reasoningEffort?: string;
|
|
74
|
+
}): Promise<GateResolvedConfig>;
|
|
75
|
+
/** Atomic provenance re-resolution of a model selector (undefined = inherit). */
|
|
76
|
+
resolveDetailed(selector: string | undefined): GateDetailedRoute;
|
|
77
|
+
}
|
|
78
|
+
/** One gate outcome. Denies carry a stable code and a human reason. */
|
|
79
|
+
export type GateDecision = {
|
|
80
|
+
readonly action: 'pass';
|
|
81
|
+
readonly notices: readonly string[];
|
|
82
|
+
readonly clearBlocked?: boolean;
|
|
83
|
+
readonly overlay?: OverlayTuple;
|
|
84
|
+
} | {
|
|
85
|
+
readonly action: 'deny';
|
|
86
|
+
readonly code: DenyCode;
|
|
87
|
+
readonly reason: string;
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* Evaluate the gate steps 0-5 for one pin (or an unreadable one — corrupt
|
|
91
|
+
* pins deny before any environment probe). Every deny the caller persists as
|
|
92
|
+
* `resume.state='blocked'`; an all-passing evaluation clears it.
|
|
93
|
+
* @param pin - the read pin (a {@link CorruptPin} denies `PIN_UNREADABLE`).
|
|
94
|
+
* @param env - probed environment facts and availability resolvers.
|
|
95
|
+
* @param policy - the LIVE policy read at evaluation time.
|
|
96
|
+
*/
|
|
97
|
+
export declare function evaluateGate(pin: ResumePin | CorruptPin, env: GateEnv, policy: ResumePolicy): Promise<GateDecision>;
|
|
98
|
+
//# sourceMappingURL=gate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gate.d.ts","sourceRoot":"","sources":["../src/gate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EACV,UAAU,EACX,MAAM,YAAY,CAAA;AACnB,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,UAAU,CAAA;AACvD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAE/C,+EAA+E;AAC/E,MAAM,MAAM,QAAQ,GAChB,cAAc,GACd,gBAAgB,GAChB,mBAAmB,GACnB,yBAAyB,GACzB,4BAA4B,GAC5B,mBAAmB,GACnB,oBAAoB,GACpB,qBAAqB,CAAA;AAEzB,uEAAuE;AACvE,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAC7C,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CACxC;AAED,6DAA6D;AAC7D,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,GAAG,EAAE,OAAO,GAAG,SAAS,GAAG,SAAS,CAAA;IAC7C,QAAQ,CAAC,KAAK,EACV;QAAE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAA;KAAE,GAC1F,SAAS,CAAA;CACd;AAED,iFAAiF;AACjF,MAAM,WAAW,OAAO;IACtB,wEAAwE;IACxE,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAA;IAC/B,qDAAqD;IACrD,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAA;IAC3B,yEAAyE;IACzE,QAAQ,CAAC,UAAU,EAAE;QACnB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;QACvB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;QAC7B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;KACxB,CAAA;IACD;;;;OAIG;IACH,QAAQ,CAAC,4BAA4B,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAAA;IAChE;;;;OAIG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE;QAAE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IAC5G,4DAA4D;IAC5D,QAAQ,CAAC,iBAAiB,EAAE,WAAW,CAAC,MAAM,CAAC,CAAA;IAC/C;;;OAGG;IACH,iBAAiB,CAAC,MAAM,EAAE;QACxB,QAAQ,EAAE,MAAM,CAAA;QAChB,KAAK,EAAE,MAAM,CAAA;QACb,SAAS,CAAC,EAAE,MAAM,CAAA;QAClB,eAAe,CAAC,EAAE,MAAM,CAAA;KACzB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAA;IAC/B,iFAAiF;IACjF,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,GAAG,iBAAiB,CAAA;CACjE;AAED,uEAAuE;AACvE,MAAM,MAAM,YAAY,GACpB;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IAAC,QAAQ,CAAC,YAAY,CAAC,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,YAAY,CAAA;CAAE,GAClI;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAA;AAqFjF;;;;;;;GAOG;AACH,wBAAsB,YAAY,CAChC,GAAG,EAAE,SAAS,GAAG,UAAU,EAC3B,GAAG,EAAE,OAAO,EACZ,MAAM,EAAE,YAAY,GACnB,OAAO,CAAC,YAAY,CAAC,CAwHvB"}
|
package/lib/gate.js
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The PURE resume gate (plan §4.6): re-evaluates one pin against current
|
|
3
|
+
* environment facts on every cold resume. No cordis, no IO of its own — the
|
|
4
|
+
* environment supplies already-probed facts and throwing availability
|
|
5
|
+
* resolvers; the caller (the plugin) persists any deny to the pin BEFORE the
|
|
6
|
+
* decision is returned (durability ordering) and is also responsible for
|
|
7
|
+
* skipping the gate entirely when no pin exists (legacy/foreign child).
|
|
8
|
+
*
|
|
9
|
+
* A persisted `resume.state='blocked'` never short-circuits: policy flips and
|
|
10
|
+
* recovered conditions are authoritative, the stored state is derived.
|
|
11
|
+
*
|
|
12
|
+
* @module @dsh-cc/subagent-resume-pins/gate
|
|
13
|
+
*/
|
|
14
|
+
/** Field-by-field tuple comparison WITH absence semantics (`null` = absent). */
|
|
15
|
+
function tupleDrift(pinned, current) {
|
|
16
|
+
if (current.provider !== pinned.provider)
|
|
17
|
+
return `provider ${pinned.provider} -> ${current.provider}`;
|
|
18
|
+
if (current.model !== pinned.model)
|
|
19
|
+
return `model ${pinned.model} -> ${current.model}`;
|
|
20
|
+
if ((current.reasoningEffort ?? null) !== pinned.reasoningEffort) {
|
|
21
|
+
return `reasoningEffort ${JSON.stringify(pinned.reasoningEffort)} -> ${JSON.stringify(current.reasoningEffort ?? null)}`;
|
|
22
|
+
}
|
|
23
|
+
if ((current.maxTokens ?? null) !== pinned.maxTokens) {
|
|
24
|
+
return `maxTokens ${JSON.stringify(pinned.maxTokens)} -> ${JSON.stringify(current.maxTokens ?? null)}`;
|
|
25
|
+
}
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
/** Re-resolution of the pinned route: throws when unavailable. */
|
|
29
|
+
function resolvePinned(env, pin) {
|
|
30
|
+
return env.resolveCallConfig({
|
|
31
|
+
provider: pin.effective.provider,
|
|
32
|
+
model: pin.effective.model,
|
|
33
|
+
...(pin.effective.reasoningEffort !== null ? { reasoningEffort: pin.effective.reasoningEffort } : {}),
|
|
34
|
+
...(pin.effective.maxTokens !== null ? { maxTokens: pin.effective.maxTokens } : {}),
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
/** Alias drift: the selector no longer resolves (to the pinned model) via alias. */
|
|
38
|
+
function aliasDrift(env, pin) {
|
|
39
|
+
if (pin.modelSelector.via !== 'alias')
|
|
40
|
+
return undefined;
|
|
41
|
+
const detailed = env.resolveDetailed(pin.modelSelector.raw);
|
|
42
|
+
if (detailed.via !== 'alias' || detailed.route?.model === undefined) {
|
|
43
|
+
return `alias "${pin.modelSelector.raw}" no longer resolves`;
|
|
44
|
+
}
|
|
45
|
+
if (detailed.route.model !== pin.effective.model) {
|
|
46
|
+
return `alias "${pin.modelSelector.raw}" now resolves to "${detailed.route.model}"`;
|
|
47
|
+
}
|
|
48
|
+
if (detailed.route.provider !== undefined && detailed.route.provider !== pin.effective.provider) {
|
|
49
|
+
return `alias "${pin.modelSelector.raw}" now routes to provider "${detailed.route.provider}"`;
|
|
50
|
+
}
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The route-current fallback (§4.6 step 5): resolve the selector fresh via
|
|
55
|
+
* `resolveDetailed`, overlay the resolution onto the CURRENT parent route
|
|
56
|
+
* (`env.currentRoute` — never the pinned tuple), and preflight the complete
|
|
57
|
+
* tuple atomically. Alias drift detection is unchanged; the fallback route
|
|
58
|
+
* itself is always the current one, so an alias that no longer resolves falls
|
|
59
|
+
* back to the parent's current default route rather than the pinned tuple.
|
|
60
|
+
* An unresolvable current route (no parent route on record, or a preflight
|
|
61
|
+
* failure) resolves `undefined`.
|
|
62
|
+
*/
|
|
63
|
+
async function currentTuple(env, pin) {
|
|
64
|
+
const base = { ...env.currentRoute };
|
|
65
|
+
const detailed = env.resolveDetailed(pin.modelSelector.via === 'inherit' ? undefined : pin.modelSelector.raw);
|
|
66
|
+
const route = detailed.route;
|
|
67
|
+
if (route?.model !== undefined)
|
|
68
|
+
base.model = route.model;
|
|
69
|
+
if (route?.provider !== undefined)
|
|
70
|
+
base.provider = route.provider;
|
|
71
|
+
if (route?.reasoningEffort !== undefined)
|
|
72
|
+
base.reasoningEffort = route.reasoningEffort;
|
|
73
|
+
if (base.provider === undefined || base.model === undefined)
|
|
74
|
+
return undefined;
|
|
75
|
+
try {
|
|
76
|
+
const resolved = await env.resolveCallConfig({
|
|
77
|
+
provider: base.provider,
|
|
78
|
+
model: base.model,
|
|
79
|
+
...(base.reasoningEffort !== undefined ? { reasoningEffort: base.reasoningEffort } : {}),
|
|
80
|
+
...(base.maxTokens !== undefined ? { maxTokens: base.maxTokens } : {}),
|
|
81
|
+
});
|
|
82
|
+
return {
|
|
83
|
+
provider: resolved.provider,
|
|
84
|
+
model: resolved.model,
|
|
85
|
+
reasoningEffort: resolved.reasoningEffort ?? null,
|
|
86
|
+
maxTokens: resolved.maxTokens ?? null,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Evaluate the gate steps 0-5 for one pin (or an unreadable one — corrupt
|
|
95
|
+
* pins deny before any environment probe). Every deny the caller persists as
|
|
96
|
+
* `resume.state='blocked'`; an all-passing evaluation clears it.
|
|
97
|
+
* @param pin - the read pin (a {@link CorruptPin} denies `PIN_UNREADABLE`).
|
|
98
|
+
* @param env - probed environment facts and availability resolvers.
|
|
99
|
+
* @param policy - the LIVE policy read at evaluation time.
|
|
100
|
+
*/
|
|
101
|
+
export async function evaluateGate(pin, env, policy) {
|
|
102
|
+
// Step 1 (readability — probed before anything else: a corrupt pin has no
|
|
103
|
+
// fields to evaluate against, and fail-closed beats every notice path).
|
|
104
|
+
if ('kind' in pin) {
|
|
105
|
+
return { action: 'deny', code: 'PIN_UNREADABLE', reason: `[PIN_UNREADABLE] resume pin is unreadable (${pin.reason}); refusing to resume` };
|
|
106
|
+
}
|
|
107
|
+
const resumedPin = pin;
|
|
108
|
+
const notices = [];
|
|
109
|
+
// Step 0: the persisted session must exist — otherwise the pin is an
|
|
110
|
+
// orphan of an aborted spawn and the id is unaddressable.
|
|
111
|
+
if (!env.sessionExists) {
|
|
112
|
+
return { action: 'deny', code: 'PIN_ORPHANED', reason: '[PIN_ORPHANED] no persisted session exists for this pinned child; the spawn must have been aborted' };
|
|
113
|
+
}
|
|
114
|
+
// Step 2: workspace. A missing cwd has no fallback; a changed canonical
|
|
115
|
+
// repo identity is a policy; branch-only drift is a notice either way.
|
|
116
|
+
if (!env.cwdExists) {
|
|
117
|
+
return { action: 'deny', code: 'WORKSPACE_MISSING', reason: `[WORKSPACE_MISSING] pinned workspace ${resumedPin.workspace.cwd} no longer exists; there is no relocatable fallback` };
|
|
118
|
+
}
|
|
119
|
+
const repoIdentityChanged = env.currentGit.gitDir !== resumedPin.workspace.gitDir
|
|
120
|
+
|| env.currentGit.gitCommonDir !== resumedPin.workspace.gitCommonDir;
|
|
121
|
+
if (repoIdentityChanged) {
|
|
122
|
+
if (policy.onWorkspaceChanged === 'block') {
|
|
123
|
+
return { action: 'deny', code: 'WORKSPACE_CHANGED', reason: `[WORKSPACE_CHANGED] workspace repository identity changed since spawn (pinned ${resumedPin.workspace.gitDir}/${resumedPin.workspace.gitCommonDir}, current ${env.currentGit.gitDir}/${env.currentGit.gitCommonDir}); policy onWorkspaceChanged=block` };
|
|
124
|
+
}
|
|
125
|
+
notices.push('resumed after the workspace repository identity changed (branch/worktree re-provisioned); continuing in the current workspace');
|
|
126
|
+
}
|
|
127
|
+
if (env.currentGit.branch !== resumedPin.workspace.branch) {
|
|
128
|
+
notices.push(`workspace branch changed since spawn (pinned ${resumedPin.workspace.branch}, current ${env.currentGit.branch}); continuing on the current branch`);
|
|
129
|
+
}
|
|
130
|
+
// Step 3: definition identity (named pins only). The child keeps its
|
|
131
|
+
// PINNED persona/tool filter — the harness descriptor restores those, so
|
|
132
|
+
// nothing is re-guessed even when the file changed.
|
|
133
|
+
if (resumedPin.definition.kind === 'named' && env.currentDefinitionFingerprint !== null) {
|
|
134
|
+
if (env.currentDefinitionFingerprint !== resumedPin.definition.fingerprint) {
|
|
135
|
+
if (policy.onDefinitionChanged === 'block') {
|
|
136
|
+
return { action: 'deny', code: 'DEFINITION_CHANGED', reason: `[DEFINITION_CHANGED] definition "${resumedPin.definition.agentType}" changed since spawn and policy onDefinitionChanged=block` };
|
|
137
|
+
}
|
|
138
|
+
notices.push('resumed with changed definition (pinned persona retained)');
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// Step 4: every pinned filter name must still be restrictable — pruning an
|
|
142
|
+
// allow entry shrinks capability, pruning a deny entry widens permissions.
|
|
143
|
+
const missing = [...resumedPin.toolFilter.allow, ...resumedPin.toolFilter.deny]
|
|
144
|
+
.filter(name => !env.restrictableNames.has(name));
|
|
145
|
+
if (missing.length > 0) {
|
|
146
|
+
return { action: 'deny', code: 'PINNED_TOOL_UNAVAILABLE', reason: `[PINNED_TOOL_UNAVAILABLE] pinned tool filter names tools this composition no longer knows: ${missing.join(', ')}` };
|
|
147
|
+
}
|
|
148
|
+
// Step 5: model/route availability & drift.
|
|
149
|
+
const unavailable = (detail) => `[SUBAGENT_MODEL_UNAVAILABLE] pinned route ${resumedPin.effective.provider}/${resumedPin.effective.model} is no longer available (${detail}); unblock with subagents-resume.onUnavailableModel: 'route-current'`;
|
|
150
|
+
if (resumedPin.effective.complete) {
|
|
151
|
+
let resolved;
|
|
152
|
+
try {
|
|
153
|
+
resolved = await resolvePinned(env, pin);
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
const detail = error.message;
|
|
157
|
+
if (policy.onUnavailableModel === 'block') {
|
|
158
|
+
return { action: 'deny', code: 'SUBAGENT_MODEL_UNAVAILABLE', reason: unavailable(detail) };
|
|
159
|
+
}
|
|
160
|
+
const overlay = await currentTuple(env, pin);
|
|
161
|
+
if (overlay === undefined) {
|
|
162
|
+
return { action: 'deny', code: 'SUBAGENT_MODEL_UNAVAILABLE', reason: unavailable(`${detail}; the route-current fallback is unavailable too`) };
|
|
163
|
+
}
|
|
164
|
+
notices.push(`original model ${resumedPin.effective.provider}/${resumedPin.effective.model} unavailable; resumed with current default route ${overlay.provider}/${overlay.model} per policy`);
|
|
165
|
+
return finish({ overlay });
|
|
166
|
+
}
|
|
167
|
+
const drift = tupleDrift(resumedPin.effective, resolved) ?? aliasDrift(env, pin);
|
|
168
|
+
if (drift === undefined)
|
|
169
|
+
return finish();
|
|
170
|
+
if (policy.onUnavailableModel === 'block') {
|
|
171
|
+
return { action: 'deny', code: 'SUBAGENT_MODEL_UNAVAILABLE', reason: unavailable(drift) };
|
|
172
|
+
}
|
|
173
|
+
const overlay = await currentTuple(env, pin);
|
|
174
|
+
if (overlay === undefined) {
|
|
175
|
+
return { action: 'deny', code: 'SUBAGENT_MODEL_UNAVAILABLE', reason: unavailable(`${drift}; the route-current fallback is unavailable too`) };
|
|
176
|
+
}
|
|
177
|
+
notices.push(`original model ${resumedPin.effective.provider}/${resumedPin.effective.model} unavailable; resumed with current default route ${overlay.provider}/${overlay.model} per policy`);
|
|
178
|
+
return finish({ overlay });
|
|
179
|
+
}
|
|
180
|
+
// Degraded pin: only provider-mounted and alias-drift are checkable.
|
|
181
|
+
try {
|
|
182
|
+
await resolvePinned(env, pin);
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
const detail = error.message;
|
|
186
|
+
if (policy.onUnavailableModel === 'block') {
|
|
187
|
+
return { action: 'deny', code: 'SUBAGENT_MODEL_UNAVAILABLE', reason: unavailable(detail) };
|
|
188
|
+
}
|
|
189
|
+
const overlay = await currentTuple(env, pin);
|
|
190
|
+
if (overlay === undefined) {
|
|
191
|
+
return { action: 'deny', code: 'SUBAGENT_MODEL_UNAVAILABLE', reason: unavailable(`${detail}; the route-current fallback is unavailable too`) };
|
|
192
|
+
}
|
|
193
|
+
notices.push(`original model ${resumedPin.effective.provider}/${resumedPin.effective.model} unavailable; resumed with current default route ${overlay.provider}/${overlay.model} per policy`);
|
|
194
|
+
return finish({ overlay });
|
|
195
|
+
}
|
|
196
|
+
const drift = aliasDrift(env, pin);
|
|
197
|
+
if (drift !== undefined) {
|
|
198
|
+
if (policy.onUnavailableModel === 'block') {
|
|
199
|
+
return { action: 'deny', code: 'SUBAGENT_MODEL_UNAVAILABLE', reason: unavailable(drift) };
|
|
200
|
+
}
|
|
201
|
+
const overlay = await currentTuple(env, pin);
|
|
202
|
+
if (overlay === undefined) {
|
|
203
|
+
return { action: 'deny', code: 'SUBAGENT_MODEL_UNAVAILABLE', reason: unavailable(`${drift}; the route-current fallback is unavailable too`) };
|
|
204
|
+
}
|
|
205
|
+
notices.push(`original model ${resumedPin.effective.provider}/${resumedPin.effective.model} unavailable; resumed with current default route ${overlay.provider}/${overlay.model} per policy`);
|
|
206
|
+
return finish({ overlay });
|
|
207
|
+
}
|
|
208
|
+
return finish();
|
|
209
|
+
function finish(extra = {}) {
|
|
210
|
+
return {
|
|
211
|
+
action: 'pass',
|
|
212
|
+
notices,
|
|
213
|
+
...resumedPin.resume.state === 'blocked' ? { clearBlocked: true } : {},
|
|
214
|
+
...extra.overlay !== undefined ? { overlay: extra.overlay } : {},
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
//# sourceMappingURL=gate.js.map
|