trackfw 6.9.1 → 7.0.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/README.md +0 -3
- package/package.json +1 -1
- package/src/branch/runner.js +33 -20
- package/src/changelog/index.js +114 -0
- package/src/commands/branch.js +12 -9
- package/src/commands/changelog.js +29 -0
- package/src/commands/commit.js +60 -0
- package/src/commands/discover.js +69 -0
- package/src/commands/index.js +22 -4
- package/src/commands/integrations.js +7 -0
- package/src/commands/ship.js +4 -3
- package/src/commands/thirdparty.js +384 -0
- package/src/commands/update-harness.js +3 -1
- package/src/commands/update.js +3 -1
- package/src/commit/runner.js +352 -0
- package/src/config/index.js +27 -0
- package/src/generators/hooks.js +479 -2
- package/src/generators/init.js +28 -6
- package/src/i18n/locales/en-US.json +1 -19
- package/src/i18n/locales/es-ES.json +1 -19
- package/src/i18n/locales/pt-BR.json +1 -19
- package/src/integrations/index.js +19 -2
- package/src/integrations/manager.js +15 -2
- package/src/integrations/render.js +102 -4
- package/src/lib/unknown-command.js +82 -0
- package/src/ship/runner.js +213 -26
- package/src/thirdparty/fetch.js +140 -0
- package/src/thirdparty/markers.js +207 -0
- package/src/thirdparty/provenance.js +114 -0
- package/src/thirdparty/quarantine.js +113 -0
- package/src/thirdparty/references.js +194 -0
- package/src/validator/index.js +715 -16
- package/src/commands/plugins.js +0 -184
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Node port of internal/commands/integrations_thirdparty.go — the
|
|
4
|
+
// `third-party fetch`/`third-party install` two-phase quarantine gate,
|
|
5
|
+
// reachable from both `trackfw agents third-party` and
|
|
6
|
+
// `trackfw skills third-party` (D1). See:
|
|
7
|
+
// docs/adr/ADR-2026-08-15-gate-de-duas-fases-para-artefatos-de-terceiro-quarentena-parecer-vinculado-por-checksum-e-deteccao-por-proveniencia-versionada.md
|
|
8
|
+
|
|
9
|
+
const { Command } = require('commander')
|
|
10
|
+
|
|
11
|
+
const identityStore = require('../identity')
|
|
12
|
+
const { items: catalogItems } = require('../integrations/catalog')
|
|
13
|
+
const { buildPlans, IntegrationManager } = require('../integrations')
|
|
14
|
+
const thirdPartyFetchModule = require('../thirdparty/fetch')
|
|
15
|
+
const { checkMarkers, checksum: sha256hex } = require('../thirdparty/markers')
|
|
16
|
+
const { quarantinePath, newQuarantineEntry, writeQuarantine, readQuarantine, decodeContent } = require('../thirdparty/quarantine')
|
|
17
|
+
const { loadProvenance, verifyApproval, upsertProvenanceEntry } = require('../thirdparty/provenance')
|
|
18
|
+
const { upsertThirdPartyReference, normalizeThirdPartyContent, resolveThirdPartySkillDestination } = require('../thirdparty/references')
|
|
19
|
+
|
|
20
|
+
// api is the module.exports object itself, referenced internally via
|
|
21
|
+
// `api.thirdPartyFetch(...)` so tests can substitute the network fetch by
|
|
22
|
+
// reassigning the exported property in place — mirrors the
|
|
23
|
+
// identityWizard.runIdentityWizard monkey-patch pattern already used by
|
|
24
|
+
// npm/tests/identity-wizard.test.js, and the package-var substitution used
|
|
25
|
+
// by internal/commands/integrations_thirdparty.go:thirdPartyFetch on the Go
|
|
26
|
+
// side (tests substitute it instead of hitting real network, keeping
|
|
27
|
+
// fetch-command tests compatible with TRACKFW_DISABLE_EXTERNAL_COMMANDS=1).
|
|
28
|
+
const api = {}
|
|
29
|
+
|
|
30
|
+
// THIRD_PARTY_PROVENANCE_RULE is the name of the trackfw validate rule that
|
|
31
|
+
// is the real (git-anchored) enforcement behind the orchestrator-session
|
|
32
|
+
// guardrail below. Named as a constant so the guardrail message and any
|
|
33
|
+
// future test asserting its wording never drift apart silently. Mirrors
|
|
34
|
+
// internal/commands/integrations_thirdparty.go:thirdPartyProvenanceRule.
|
|
35
|
+
const THIRD_PARTY_PROVENANCE_RULE = 'thirdparty_artifact_has_provenance'
|
|
36
|
+
|
|
37
|
+
// THIRD_PARTY_GLOBAL_SCOPE_WARNING/REFUSAL are the D4-bis literal strings
|
|
38
|
+
// for --scope global: printed/returned VERBATIM by all 3 CLIs (the
|
|
39
|
+
// roadmap's AC requires identical wording). Mirrors
|
|
40
|
+
// internal/commands/integrations_thirdparty.go:thirdPartyGlobalScopeWarning/thirdPartyGlobalScopeRefusal
|
|
41
|
+
// — keep these two strings byte-identical to the Go constants.
|
|
42
|
+
const THIRD_PARTY_GLOBAL_SCOPE_WARNING =
|
|
43
|
+
'warning: --scope global installs outside the project tree; this artifact will NEVER be verified by ' +
|
|
44
|
+
'`trackfw validate` (the "' + THIRD_PARTY_PROVENANCE_RULE + '" rule only scans the project\'s own manifest — ' +
|
|
45
|
+
'an artifact under a home directory is invisible to it, per ADR-2026-08-12).'
|
|
46
|
+
const THIRD_PARTY_GLOBAL_SCOPE_REFUSAL =
|
|
47
|
+
'install to --scope global requires --yes-global-scope-unverified as its own explicit confirmation (D4-bis), ' +
|
|
48
|
+
'distinct from --yes-i-trust-this-source: it confirms you understand `trackfw validate` will never verify this installation'
|
|
49
|
+
|
|
50
|
+
// SLUG_PATTERN mirrors internal/commands/integrations_thirdparty.go:thirdPartySlugPattern.
|
|
51
|
+
const SLUG_PATTERN = /^[a-z0-9][a-z0-9._-]{0,63}$/
|
|
52
|
+
|
|
53
|
+
// CHECKSUM_PATTERN mirrors internal/commands/integrations_thirdparty.go:thirdPartyChecksumPattern.
|
|
54
|
+
const CHECKSUM_PATTERN = /^[a-f0-9]{64}$/
|
|
55
|
+
|
|
56
|
+
function csv(value) {
|
|
57
|
+
return String(value).split(',').map(entry => entry.trim()).filter(Boolean)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// checkOrchestratorGuardrail implements D2: TRACKFW_ORCHESTRATOR_SESSION is
|
|
61
|
+
// a guardrail against accidental invocation from a plain terminal, never a
|
|
62
|
+
// security control — it is trivially set by anyone with shell access. The
|
|
63
|
+
// real enforcement is the THIRD_PARTY_PROVENANCE_RULE check in `trackfw
|
|
64
|
+
// validate` (ML-3A), which is git-anchored per ADR-2026-08-12. This message
|
|
65
|
+
// must never present the env var as prevention. Mirrors
|
|
66
|
+
// internal/commands/integrations_thirdparty.go:checkOrchestratorGuardrail.
|
|
67
|
+
function checkOrchestratorGuardrail() {
|
|
68
|
+
if (process.env.TRACKFW_ORCHESTRATOR_SESSION) return
|
|
69
|
+
throw new Error(
|
|
70
|
+
'refused: TRACKFW_ORCHESTRATOR_SESSION is not set. This is a guardrail against accidental ' +
|
|
71
|
+
'invocation from a plain terminal, not a security control — it does not resist anyone who ' +
|
|
72
|
+
`already has shell access. The real enforcement is the "${THIRD_PARTY_PROVENANCE_RULE}" rule checked by ` +
|
|
73
|
+
'`trackfw validate`, which detects any third-party artifact committed without a matching, checksum-linked ' +
|
|
74
|
+
'provenance entry. If this is an orchestrated agent session, set TRACKFW_ORCHESTRATOR_SESSION=1',
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function thirdPartyEntryKind(kind) {
|
|
79
|
+
return kind === 'agents' ? 'agent' : 'skill'
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// deriveSlug produces a filesystem-safe slug from a quarantined artifact's
|
|
83
|
+
// source URL when --slug is not given. Mirrors
|
|
84
|
+
// internal/commands/integrations_thirdparty.go:deriveSlug.
|
|
85
|
+
function deriveSlug(rawURL) {
|
|
86
|
+
let parsed
|
|
87
|
+
try {
|
|
88
|
+
parsed = new URL(rawURL)
|
|
89
|
+
} catch (err) {
|
|
90
|
+
throw new Error(`cannot derive slug from URL "${rawURL}": ${err.message}`)
|
|
91
|
+
}
|
|
92
|
+
let base = parsed.pathname.split('/').filter(Boolean).pop() || ''
|
|
93
|
+
const dot = base.lastIndexOf('.')
|
|
94
|
+
if (dot > 0) base = base.slice(0, dot)
|
|
95
|
+
base = base.toLowerCase()
|
|
96
|
+
let out = ''
|
|
97
|
+
for (const ch of base) {
|
|
98
|
+
if ((ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9') || ch === '-' || ch === '_' || ch === '.') out += ch
|
|
99
|
+
else out += '-'
|
|
100
|
+
}
|
|
101
|
+
const slug = out.replace(/^[-._]+/, '').replace(/[-._]+$/, '')
|
|
102
|
+
if (!slug || !SLUG_PATTERN.test(slug)) throw new Error(`cannot derive a safe slug from URL "${rawURL}"; pass --slug explicitly`)
|
|
103
|
+
return slug
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// resolveThirdPartyScope is deliberately separate from resolveScope
|
|
107
|
+
// (../commands/integrations.js): third-party's default is "project" (D4),
|
|
108
|
+
// the opposite of the catalog's "global" default. Explicit --scope is
|
|
109
|
+
// detected via `options.scope !== undefined`, never by comparing the
|
|
110
|
+
// value against "project" (that comparison cannot distinguish an explicit
|
|
111
|
+
// `--scope project` from the flag's unset default). Mirrors
|
|
112
|
+
// internal/commands/integrations_thirdparty.go:resolveThirdPartyScope.
|
|
113
|
+
function resolveThirdPartyScope(options) {
|
|
114
|
+
if (options.scope !== undefined) {
|
|
115
|
+
if (options.scope !== 'project' && options.scope !== 'global') throw new Error(`invalid --scope "${options.scope}": use project or global`)
|
|
116
|
+
return options.scope
|
|
117
|
+
}
|
|
118
|
+
return 'project'
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function executeThirdPartyFetch(kind, url, options) {
|
|
122
|
+
checkOrchestratorGuardrail()
|
|
123
|
+
const raw = await api.thirdPartyFetch(url)
|
|
124
|
+
const matched = checkMarkers(raw)
|
|
125
|
+
if (matched.length && !options.forceThirdpartyMarkers) {
|
|
126
|
+
throw new Error(
|
|
127
|
+
`refused: content matches boundary-redefinition marker(s) [${matched.join(' ')}] (D3); pass --force-thirdparty-markers ` +
|
|
128
|
+
'to quarantine it anyway (recorded in marker_check, never installed without approval)',
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const manager = new IntegrationManager()
|
|
133
|
+
const entry = newQuarantineEntry(url, raw, matched, thirdPartyEntryKind(kind), options.targets || [])
|
|
134
|
+
writeQuarantine(manager.roots.project, entry)
|
|
135
|
+
|
|
136
|
+
console.log(`quarantined: ${quarantinePath(manager.roots.project, entry.checksum_sha256)}`)
|
|
137
|
+
console.log(`checksum: ${entry.checksum_sha256}`)
|
|
138
|
+
if (matched.length) console.log(`warning: marker check failed (matched=[${matched.join(' ')}]); --force-thirdparty-markers was used`)
|
|
139
|
+
console.log(
|
|
140
|
+
'next: obtain a favorable hades-tf review, record its provenance entry keyed by the resolved destination(s), then run ' +
|
|
141
|
+
`\`${kind} third-party install --checksum ${entry.checksum_sha256} --targets <t1,t2> [--apply-to <agent-id,...>]\``,
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function executeThirdPartyInstall(kind, options) {
|
|
146
|
+
checkOrchestratorGuardrail()
|
|
147
|
+
if (!options.checksum) throw new Error('install requires --checksum')
|
|
148
|
+
if (!CHECKSUM_PATTERN.test(options.checksum)) {
|
|
149
|
+
throw new Error(`invalid --checksum "${options.checksum}": expected a 64-character lowercase SHA-256 hex digest`)
|
|
150
|
+
}
|
|
151
|
+
if (!options.targets || !options.targets.length) throw new Error('install requires --targets')
|
|
152
|
+
const scope = resolveThirdPartyScope(options)
|
|
153
|
+
// D4-bis — print the warning as early as possible, before any other
|
|
154
|
+
// output, so it is visible even if a later step aborts the command.
|
|
155
|
+
if (scope === 'global') console.log(THIRD_PARTY_GLOBAL_SCOPE_WARNING)
|
|
156
|
+
|
|
157
|
+
const manager = new IntegrationManager()
|
|
158
|
+
const projectRoot = manager.roots.project
|
|
159
|
+
|
|
160
|
+
const entry = readQuarantine(projectRoot, options.checksum)
|
|
161
|
+
const content = decodeContent(entry)
|
|
162
|
+
|
|
163
|
+
// D8c / TOCTOU close: the quarantine record's filename IS its checksum,
|
|
164
|
+
// but that alone does not prove content_base64 hasn't been edited in
|
|
165
|
+
// place since approval. Recompute over the decoded bytes and require
|
|
166
|
+
// both the record's own field and the caller-supplied --checksum to
|
|
167
|
+
// agree with it.
|
|
168
|
+
const recomputed = sha256hex(content)
|
|
169
|
+
if (recomputed !== options.checksum || entry.checksum_sha256 !== options.checksum) {
|
|
170
|
+
throw new Error(`refused: quarantined content for "${entry.url}" no longer matches checksum ${options.checksum} (TOCTOU guard, D8c)`)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
let slug = options.slug
|
|
174
|
+
if (!slug) {
|
|
175
|
+
slug = deriveSlug(entry.url)
|
|
176
|
+
} else if (!SLUG_PATTERN.test(slug)) {
|
|
177
|
+
throw new Error(`invalid --slug "${slug}": use lowercase alphanumerics, '.', '_' or '-'`)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const resolvedTargets = options.targets.map(targetID => {
|
|
181
|
+
const { destination, surfaceID } = resolveThirdPartySkillDestination(targetID, scope, slug)
|
|
182
|
+
return { targetID, surfaceID, destination }
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
// D5/AC3 preconditions for --apply-to are validated here, BEFORE any
|
|
186
|
+
// write happens (including the skill file below) — failing everything up
|
|
187
|
+
// front avoids leaving a partial state (skill file written, no reference
|
|
188
|
+
// injected) on a precondition failure.
|
|
189
|
+
let identityConfig
|
|
190
|
+
if (options.applyTo && options.applyTo.length) {
|
|
191
|
+
identityConfig = identityStore.load(manager.roots.global)
|
|
192
|
+
for (const agentID of options.applyTo) {
|
|
193
|
+
if (!catalogItems('agents').some(item => item.id === agentID)) throw new Error(`unknown agent item "${agentID}"`)
|
|
194
|
+
for (const rt of resolvedTargets) {
|
|
195
|
+
const agentPlans = buildPlans('agents', { targets: [rt.targetID], items: [agentID], scope, identity: identityConfig, projectRoot })
|
|
196
|
+
if (!agentPlans.length) throw new Error(`target ${rt.targetID} has no supported agents surface for item "${agentID}"`)
|
|
197
|
+
const inspection = manager.inspect([agentPlans[0]])[0]
|
|
198
|
+
// ADR imprecision found and resolved here (reported, not silently
|
|
199
|
+
// worked around — mirrors the Go reference's own documented
|
|
200
|
+
// resolution): D5/D8 never say which scope the referencing agent
|
|
201
|
+
// artifact must be at, and D4 gives third-party a DIFFERENT default
|
|
202
|
+
// scope (project) than the catalog default (global) — so the common
|
|
203
|
+
// case has no project-scoped agent artifact to inject into.
|
|
204
|
+
// Resolution: require the agent to already be installed, owned, and
|
|
205
|
+
// NOT hand-modified at the SAME scope as the skill; fail loudly with
|
|
206
|
+
// the exact remediation instead of silently skipping (AC3 forbids
|
|
207
|
+
// silent decisions).
|
|
208
|
+
if (!inspection.managed || inspection.state === 'not-installed') {
|
|
209
|
+
throw new Error(
|
|
210
|
+
`cannot attach reference: agent "${agentID}" is not installed at --scope ${scope} for target ${rt.targetID}; run ` +
|
|
211
|
+
`\`trackfw agents install --scope ${scope} --targets ${rt.targetID} --items ${agentID}\` first`,
|
|
212
|
+
)
|
|
213
|
+
}
|
|
214
|
+
if (inspection.state === 'modified') {
|
|
215
|
+
throw new Error(
|
|
216
|
+
`cannot attach reference: agent "${agentID}" at --scope ${scope} for target ${rt.targetID} was modified outside trackfw; run ` +
|
|
217
|
+
`\`trackfw agents update --scope ${scope} --targets ${rt.targetID} --items ${agentID} --force\` first`,
|
|
218
|
+
)
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// AC1 — always show content and every resolved destination before
|
|
225
|
+
// writing anything, in both interactive and non-interactive mode.
|
|
226
|
+
console.log(`URL: ${entry.url}\nChecksum: ${options.checksum}\n\n--- content ---\n${content.toString('utf8')}\n--- end content ---\n`)
|
|
227
|
+
console.log(`Resolved destination(s) (scope=${scope}):`)
|
|
228
|
+
for (const rt of resolvedTargets) console.log(` ${rt.targetID}: ${rt.destination}`)
|
|
229
|
+
|
|
230
|
+
if (!process.stdin.isTTY) {
|
|
231
|
+
if (!options.yesITrustThisSource) throw new Error('install requires --yes-i-trust-this-source in non-interactive mode (AC1)')
|
|
232
|
+
} else if (!options.yesITrustThisSource) {
|
|
233
|
+
const { confirm } = require('@inquirer/prompts')
|
|
234
|
+
const confirmed = await confirm({ message: 'Install this third-party content at the destination(s) shown above?' })
|
|
235
|
+
if (!confirmed) throw new Error('install cancelled')
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// D4-bis — global scope requires ITS OWN explicit confirmation, beyond
|
|
239
|
+
// --yes-i-trust-this-source (decision by KG, 2026-08-15, superseding the
|
|
240
|
+
// ML-3A choice to collapse both into --yes-i-trust-this-source).
|
|
241
|
+
if (scope === 'global' && !options.yesGlobalScopeUnverified) {
|
|
242
|
+
throw new Error(THIRD_PARTY_GLOBAL_SCOPE_REFUSAL)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// D8c — the TOCTOU-closing approval check, verified per resolved
|
|
246
|
+
// destination (provenance is keyed by destination, not by checksum alone
|
|
247
|
+
// — a checksum approved for one destination is not automatically
|
|
248
|
+
// approved for another).
|
|
249
|
+
for (const rt of resolvedTargets) {
|
|
250
|
+
try {
|
|
251
|
+
verifyApproval(projectRoot, options.checksum, rt.destination)
|
|
252
|
+
} catch (err) {
|
|
253
|
+
throw new Error(`not approved for ${rt.destination}: ${err.message}`)
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// D3 — a failed marker check is not fatal to fetch
|
|
258
|
+
// (--force-thirdparty-markers already overrode that refusal), but install
|
|
259
|
+
// requires the approver to have knowingly recorded marker_override in the
|
|
260
|
+
// provenance entry for each destination.
|
|
261
|
+
if (entry.marker_check.result === 'fail') {
|
|
262
|
+
const prov = loadProvenance(projectRoot)
|
|
263
|
+
for (const rt of resolvedTargets) {
|
|
264
|
+
const provEntry = prov.entries[rt.destination]
|
|
265
|
+
if (!provEntry || !provEntry.marker_override) {
|
|
266
|
+
throw new Error(
|
|
267
|
+
`refused: ${rt.destination} failed the D3 boundary marker check (matched=[${entry.marker_check.matched_markers.join(' ')}]) ` +
|
|
268
|
+
'and its provenance entry lacks marker_override=true',
|
|
269
|
+
)
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Write the skill file(s) — always through IntegrationManager, never a
|
|
275
|
+
// raw fs write. The claim kind is always "skills" regardless of which
|
|
276
|
+
// lifecycle ("skills"/"agents") invoked this command: the artifact always
|
|
277
|
+
// lives under "skills/thirdparty/" per D5.
|
|
278
|
+
const normalized = normalizeThirdPartyContent(content)
|
|
279
|
+
const plans = resolvedTargets.map(rt => ({
|
|
280
|
+
claim: { target: rt.targetID, surface: rt.surfaceID, scope, kind: 'skills', item: `thirdparty-${slug}`, origin: 'thirdparty' },
|
|
281
|
+
destination: rt.destination,
|
|
282
|
+
content: normalized,
|
|
283
|
+
catalogVersion: `thirdparty:${options.checksum.slice(0, 12)}`,
|
|
284
|
+
supportLevel: 'native',
|
|
285
|
+
}))
|
|
286
|
+
manager.install(plans, {})
|
|
287
|
+
|
|
288
|
+
// D2-bis — record installed_sha256 (SHA-256 of the NORMALIZED bytes just
|
|
289
|
+
// installed) on each destination's existing provenance entry, now that
|
|
290
|
+
// the install actually succeeded. checksum_sha256 (the raw-bytes D8c
|
|
291
|
+
// approval anchor, written externally by the approver) is left
|
|
292
|
+
// untouched — only installed_sha256 is added/overwritten. rt.destination
|
|
293
|
+
// is already the provenance key: the project-root-relative (pre-resolve)
|
|
294
|
+
// string resolveThirdPartySkillDestination returns, the same value
|
|
295
|
+
// verifyApproval was just called with above.
|
|
296
|
+
// Field order matters here for cross-CLI byte parity
|
|
297
|
+
// (scripts/check-thirdparty-parity.sh diffs thirdparty-provenance.json
|
|
298
|
+
// verbatim): url, checksum_sha256, installed_sha256, installed_at,
|
|
299
|
+
// approved_by, review_reference, scope, marker_override — mirrors Go's
|
|
300
|
+
// ProvenanceEntry struct field order and Python's _ENTRY_FIELD_ORDER
|
|
301
|
+
// (provenance.py), NOT plain object-spread insertion order (which would
|
|
302
|
+
// append installed_sha256 at the end instead of right after
|
|
303
|
+
// checksum_sha256).
|
|
304
|
+
const installedSHA256 = sha256hex(normalized)
|
|
305
|
+
for (const rt of resolvedTargets) {
|
|
306
|
+
const prov = loadProvenance(projectRoot)
|
|
307
|
+
const existing = prov.entries[rt.destination] || {}
|
|
308
|
+
const provEntry = {
|
|
309
|
+
url: existing.url,
|
|
310
|
+
checksum_sha256: existing.checksum_sha256,
|
|
311
|
+
installed_sha256: installedSHA256,
|
|
312
|
+
installed_at: existing.installed_at,
|
|
313
|
+
approved_by: existing.approved_by,
|
|
314
|
+
review_reference: existing.review_reference,
|
|
315
|
+
scope: existing.scope,
|
|
316
|
+
marker_override: existing.marker_override,
|
|
317
|
+
}
|
|
318
|
+
for (const key of Object.keys(provEntry)) {
|
|
319
|
+
if (provEntry[key] === undefined) delete provEntry[key]
|
|
320
|
+
}
|
|
321
|
+
upsertProvenanceEntry(projectRoot, rt.destination, provEntry)
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// D5 — attach references to the requested catalog agent artifacts, at
|
|
325
|
+
// the SAME scope as the skill file just installed. Preconditions were
|
|
326
|
+
// already validated above, before the skill file was written — this loop
|
|
327
|
+
// only performs writes.
|
|
328
|
+
if (options.applyTo && options.applyTo.length) {
|
|
329
|
+
for (const agentID of options.applyTo) {
|
|
330
|
+
for (const rt of resolvedTargets) {
|
|
331
|
+
upsertThirdPartyReference(projectRoot, rt.targetID, agentID, { slug, destination: rt.destination, url: entry.url })
|
|
332
|
+
// Re-render now so the on-disk artifact reflects the new reference
|
|
333
|
+
// immediately — not only on the next `agents update`. buildPlans
|
|
334
|
+
// picks up the registry entry just written via
|
|
335
|
+
// applyThirdPartyReferences (../integrations/index.js).
|
|
336
|
+
const agentPlans = buildPlans('agents', { targets: [rt.targetID], items: [agentID], scope, identity: identityConfig, projectRoot })
|
|
337
|
+
manager.update(agentPlans, {})
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
console.log(`installed: ${plans.length} destination(s)`)
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function createFetchCommand(kind) {
|
|
346
|
+
return new Command('fetch')
|
|
347
|
+
.argument('<url>', 'URL of the third-party content to fetch')
|
|
348
|
+
.description('Download third-party content into quarantine for review (never installs)')
|
|
349
|
+
.option('--targets <targets>', 'target CLIs this artifact is intended for (recorded for review only; confirmed again at install)', csv)
|
|
350
|
+
.option('--force-thirdparty-markers', 'override refusal on boundary-redefinition markers (D3); recorded, never silent', false)
|
|
351
|
+
.action(async (url, options) => executeThirdPartyFetch(kind, url, options))
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function createInstallCommand(kind) {
|
|
355
|
+
return new Command('install')
|
|
356
|
+
.description('Consume a quarantined artifact into its resolved destination(s), requiring a prior checksum-linked approval')
|
|
357
|
+
.option('--checksum <checksum>', 'SHA-256 checksum of the quarantined artifact (required)')
|
|
358
|
+
.option('--slug <slug>', 'destination file slug (default: derived from the quarantined URL)')
|
|
359
|
+
.option('--targets <targets>', 'target CLIs to install the skill file into (required)', csv)
|
|
360
|
+
.option('--apply-to <ids>', 'catalog agent item IDs whose rendered file gets a reference to this artifact (optional; never inferred silently — AC3)', csv)
|
|
361
|
+
.option('--scope <scope>', 'installation scope: project or global (default: project — D4)')
|
|
362
|
+
.option('--yes-i-trust-this-source', 'required in non-interactive mode (AC1)', false)
|
|
363
|
+
.option('--yes-global-scope-unverified', 'required, in addition to --yes-i-trust-this-source, for --scope global: confirms this installation will never be verified by `trackfw validate` (D4-bis)', false)
|
|
364
|
+
.action(async options => executeThirdPartyInstall(kind, options))
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// createThirdPartyCommand builds the "third-party" subcommand (fetch +
|
|
368
|
+
// install), attached under both `trackfw agents` and `trackfw skills` (D1).
|
|
369
|
+
// Mirrors internal/commands/integrations_thirdparty.go:newIntegrationsThirdPartyCmd.
|
|
370
|
+
function createThirdPartyCommand(kind) {
|
|
371
|
+
const root = new Command('third-party').description(`Fetch and install third-party ${kind} content under a two-phase quarantine gate`)
|
|
372
|
+
root.addCommand(createFetchCommand(kind))
|
|
373
|
+
root.addCommand(createInstallCommand(kind))
|
|
374
|
+
return root
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
api.createThirdPartyCommand = createThirdPartyCommand
|
|
378
|
+
api.checkOrchestratorGuardrail = checkOrchestratorGuardrail
|
|
379
|
+
api.deriveSlug = deriveSlug
|
|
380
|
+
api.resolveThirdPartyScope = resolveThirdPartyScope
|
|
381
|
+
api.THIRD_PARTY_PROVENANCE_RULE = THIRD_PARTY_PROVENANCE_RULE
|
|
382
|
+
api.thirdPartyFetch = thirdPartyFetchModule.fetch
|
|
383
|
+
|
|
384
|
+
module.exports = api
|
|
@@ -6,7 +6,7 @@ const path = require('path')
|
|
|
6
6
|
const identityStore = require('../identity')
|
|
7
7
|
const { catalog, buildPlans, IntegrationManager, globalGroupPath } = require('../integrations')
|
|
8
8
|
const { tildeify, validateTargets, buildDocument, humanReport, silenceConsole } = require('../lib/update-engine')
|
|
9
|
-
const { mergeClaudeHookArray, mergeSimpleCommandArray, mergeCopilotHookArray, generateGlobalCredentialGuardScript } = require('../generators/hooks')
|
|
9
|
+
const { mergeClaudeHookArray, mergeSimpleCommandArray, mergeCopilotHookArray, generateGlobalCredentialGuardScript, generateGlobalGitBranchGuardScript } = require('../generators/hooks')
|
|
10
10
|
|
|
11
11
|
// `trackfw update harness` is the global counterpart to `trackfw update` —
|
|
12
12
|
// see docs/cli-parity.md, "`trackfw update` vs `trackfw update harness`".
|
|
@@ -589,8 +589,10 @@ function run(options) {
|
|
|
589
589
|
if (!dryRun) {
|
|
590
590
|
if (options.json) {
|
|
591
591
|
silenceConsole(() => generateGlobalCredentialGuardScript(homeRoot))
|
|
592
|
+
silenceConsole(() => generateGlobalGitBranchGuardScript(homeRoot))
|
|
592
593
|
} else {
|
|
593
594
|
generateGlobalCredentialGuardScript(homeRoot)
|
|
595
|
+
generateGlobalGitBranchGuardScript(homeRoot)
|
|
594
596
|
}
|
|
595
597
|
}
|
|
596
598
|
|
package/src/commands/update.js
CHANGED
|
@@ -207,7 +207,7 @@ function buildProjectTargets(cwd, cfg, identityConfig, { dryRun, installMissing
|
|
|
207
207
|
|
|
208
208
|
if (include('agent-hooks')) targets.push(runFileTarget({
|
|
209
209
|
id: 'agent-hooks',
|
|
210
|
-
path: '.claude/settings.json, .codex/hooks.json, .gemini/settings.json, .kiro/hooks/trackfw-attention.json, .github/hooks/trackfw-attention.json, .cursor/hooks.json, scripts/trackfw-attention-*.sh, scripts/trackfw-credential-guard.sh',
|
|
210
|
+
path: '.claude/settings.json, .codex/hooks.json, .gemini/settings.json, .kiro/hooks/trackfw-attention.json, .github/hooks/trackfw-attention.json, .cursor/hooks.json, scripts/trackfw-attention-*.sh, scripts/trackfw-credential-guard.sh, scripts/trackfw-git-branch-guard.sh',
|
|
211
211
|
root: cwd,
|
|
212
212
|
relPaths: [
|
|
213
213
|
'.claude/settings.json',
|
|
@@ -219,11 +219,13 @@ function buildProjectTargets(cwd, cfg, identityConfig, { dryRun, installMissing
|
|
|
219
219
|
'scripts/trackfw-attention-signal.sh',
|
|
220
220
|
'scripts/trackfw-attention-cleanup.sh',
|
|
221
221
|
'scripts/trackfw-credential-guard.sh',
|
|
222
|
+
'scripts/trackfw-git-branch-guard.sh',
|
|
222
223
|
],
|
|
223
224
|
apply: (root) => {
|
|
224
225
|
hooksGen.injectHooksDetected(root);
|
|
225
226
|
hooksGen.generateAttentionScripts(cfg, root);
|
|
226
227
|
hooksGen.generateCredentialGuardScript(root);
|
|
228
|
+
hooksGen.generateGitBranchGuardScript(root);
|
|
227
229
|
},
|
|
228
230
|
dryRun,
|
|
229
231
|
installMissing,
|