trackly-cli 0.17.1 → 0.18.1
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 +7 -4
- package/contracts/trackly-apply-contract-history.json +6 -0
- package/contracts/trackly-apply-tools.json +32 -3
- package/docs/solutions/integration-issues/access-review-proposal-receipt-validation.md +151 -0
- package/docs/solutions/integration-issues/backend-owned-sensitive-answer-deletion-guard.md +88 -0
- package/docs/trackly-tools.md +10 -1
- package/lib/agent.js +2 -2
- package/mcp/apply-tools.js +717 -20
- package/mcp/server.js +96 -11
- package/npm-shrinkwrap.json +22 -21
- package/package.json +6 -4
- package/server.json +2 -2
- package/skills/trackly-apply/SKILL.md +5 -4
- package/skills/trackly-apply/references/access-probe.md +5 -0
- package/skills/trackly-apply/references/batch-orchestration.md +68 -3
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@ trackly jobs --function product
|
|
|
35
35
|
|
|
36
36
|
## At a Glance
|
|
37
37
|
|
|
38
|
-
3,800+ companies | 170K+ jobs | 40+ ATS types | CLI + MCP |
|
|
38
|
+
3,800+ companies | 170K+ jobs | 40+ ATS types | CLI + MCP | 58 local MCP tools
|
|
39
39
|
|
|
40
40
|
## CLI Commands
|
|
41
41
|
|
|
@@ -189,7 +189,10 @@ Then use natural language in any of these clients:
|
|
|
189
189
|
| trackly_get_apply_execution_snapshot | Fetch a compact bounded projection for current execution members and required profile keys |
|
|
190
190
|
| trackly_resume_parked_apply_member | Explicitly resume one parked member for a fresh non-mutating access probe |
|
|
191
191
|
| trackly_approve_apply_execution_resume | Approve one exact resume identity for an unchanged execution snapshot |
|
|
192
|
-
| trackly_advance_apply_execution | Transactionally create the next eligible immutable wave |
|
|
192
|
+
| trackly_advance_apply_execution | Transactionally create the next eligible immutable wave with frozen accessKnowledge |
|
|
193
|
+
| trackly_list_apply_access_deferments | List persistent user access deferments |
|
|
194
|
+
| trackly_defer_apply_access | Persist a job or company deferment from a Trackly jobId |
|
|
195
|
+
| trackly_clear_apply_access_deferment | Clear one discovered user access deferment |
|
|
193
196
|
| trackly_record_apply_execution_dispositions | Record typed, value-free access classifications |
|
|
194
197
|
| trackly_stop_apply_execution | Stop an execution without changing saved-job state |
|
|
195
198
|
| trackly_create_apply_batch | Freeze an exact recent-first approved batch |
|
|
@@ -282,7 +285,7 @@ trackly config --base-url http://127.0.0.1:3000 # Point at a different backend
|
|
|
282
285
|
| Job search + filters | Yes | Yes | Yes |
|
|
283
286
|
| Apply/save/dismiss | Yes | Yes | Yes |
|
|
284
287
|
| AI-powered search | Yes (trackly ask) | Yes | Yes |
|
|
285
|
-
| MCP integration | Yes (
|
|
288
|
+
| MCP integration | Yes (58 local tools) | -- | -- |
|
|
286
289
|
| Browser required | No | Yes | No |
|
|
287
290
|
| Best for | Terminal + AI agents | Visual browsing | Custom integrations |
|
|
288
291
|
|
|
@@ -296,7 +299,7 @@ Install trackly-cli (`npm install -g trackly-cli`), authenticate with `trackly l
|
|
|
296
299
|
|
|
297
300
|
**What MCP servers exist for job searching?**
|
|
298
301
|
|
|
299
|
-
trackly-cli includes a built-in MCP server with
|
|
302
|
+
trackly-cli includes a built-in MCP server with 58 tools: the complete Trackly
|
|
300
303
|
job-search and application set plus `get_more_tools`, which lets agents report
|
|
301
304
|
a missing capability. Run `trackly mcp` or use
|
|
302
305
|
`trackly agent setup --client claude`.
|
|
@@ -32,5 +32,11 @@
|
|
|
32
32
|
},
|
|
33
33
|
"3.7.6": {
|
|
34
34
|
"contractSha256": "259f4c0b52739956fced3936e36792b27960dcad820659994f3e9107453a5844"
|
|
35
|
+
},
|
|
36
|
+
"3.8.0": {
|
|
37
|
+
"contractSha256": "5404c2f210b584d4f1327d34e9753530c56a9dd963ea9524081d0d00b229df2e"
|
|
38
|
+
},
|
|
39
|
+
"3.8.1": {
|
|
40
|
+
"contractSha256": "49028b8a8e1d75a8db6e54dd91001195f8a2e50b1f0ff6554750bd243dde4c6e"
|
|
35
41
|
}
|
|
36
42
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
{
|
|
2
|
-
"contractVersion": "3.
|
|
2
|
+
"contractVersion": "3.8.1",
|
|
3
3
|
"constants": {
|
|
4
|
+
"applyExecutionProtocolVersion": "trackly-apply-execution/v3",
|
|
5
|
+
"applyAccessKnowledgeOrderingVersion": 3,
|
|
4
6
|
"applyExecutionMaxTarget": 20,
|
|
5
7
|
"applyScenarioCodes": ["browser_reclaim", "resume_upload", "resume_parser_recheck", "semantic_boolean_commit", "custom_select_commit", "multi_step_navigation", "free_text_voice", "required_error_sweep", "final_consent", "handoff_reclaim", "critical_contact_integrity", "manual_submit_boundary", "job_identity_match"],
|
|
6
8
|
"applyBrowserSurfaces": ["codex_in_app", "chrome_extension", "claude_in_chrome"],
|
|
@@ -12,7 +14,7 @@
|
|
|
12
14
|
"applyExecutionDispositionSources": ["live_probe"],
|
|
13
15
|
"applyProbeCleanupPreferences": ["never", "submitted_only", "submitted_and_probe_blockers"],
|
|
14
16
|
"applyExecutionStopReasonCodes": ["user_requested", "target_changed", "session_ended", "execution_restarted", "operator_stop"],
|
|
15
|
-
"applyBatchConflictCodes": ["state_changed", "lease_unavailable", "idempotency_key_reused", "fixed_batch_not_found", "execution_child_batch", "fixed_batch_not_active", "fixed_batch_revision_changed", "submission_in_progress", "attestation_dependencies_changed", "legacy_fixed_batch_active", "dedicated_recovery_required", "recovery_migration_not_ready"],
|
|
17
|
+
"applyBatchConflictCodes": ["state_changed", "lease_unavailable", "idempotency_key_reused", "fixed_batch_not_found", "execution_child_batch", "fixed_batch_not_active", "fixed_batch_revision_changed", "submission_in_progress", "attestation_dependencies_changed", "legacy_fixed_batch_active", "dedicated_recovery_required", "recovery_migration_not_ready", "access_deferment_limit_reached", "access_deferments_active"],
|
|
16
18
|
"fixedApplyBatchCancelReasonCodes": ["user_requested_restart"],
|
|
17
19
|
"applyCheckpointPacketPhases": ["first_pass", "delta"],
|
|
18
20
|
"applySurfaceBindingReasons": ["initial_binding", "recovery_binding"],
|
|
@@ -23,6 +25,12 @@
|
|
|
23
25
|
"applyPositiveSubmissionStrategyCodes": ["greenhouse_confirmation_redirect", "greenhouse_semantic_success", "ashby_semantic_success_banner", "ashby_submitted_state", "lever_confirmation_redirect", "lever_semantic_confirmation"],
|
|
24
26
|
"applyExecutionRecoveryEligibilityCodes": ["recoverable", "already_applied", "inactive", "revoked", "changed_requisition", "concurrently_owned", "lineage_unavailable", "retention_expired"],
|
|
25
27
|
"applyHandoffReconciliationClassifications": ["detected", "user_confirmed", "unresolved", "contradictory"],
|
|
28
|
+
"applyObservedAccessClassifications": ["open", "account_wall", "varies", "unknown"],
|
|
29
|
+
"applyAccessKnowledgeSources": ["curated_audit", "live_probe", "none"],
|
|
30
|
+
"applyAccessFreshnessStates": ["fresh", "stale", "unknown"],
|
|
31
|
+
"applySchedulingEffects": ["prefer", "neutral", "defer_while_alternatives_exist", "defer_until_cleared"],
|
|
32
|
+
"applyAccessDefermentScopes": ["job", "company", "provider"],
|
|
33
|
+
"applyAccessMatchedScopes": ["job", "company", "provider_tenant", "origin", "route", "ats_default"],
|
|
26
34
|
"applyUploadStages": ["semantic_control_discovered", "file_chooser_armed", "file_chooser_opened", "set_files_succeeded", "user_facing_filename_committed", "parser_fields_rechecked"],
|
|
27
35
|
"applyUploadFailureCodes": ["upload_capability_unavailable", "upload_stage_missing", "upload_stage_out_of_order", "upload_stage_duplicate", "control_ambiguous", "chooser_timeout", "chooser_open_without_commit", "file_missing", "file_expired", "set_files_failed", "filename_uncommitted", "unexpected_file_navigation", "parser_unsettled", "parser_field_regression"]
|
|
28
36
|
},
|
|
@@ -34,6 +42,24 @@
|
|
|
34
42
|
},
|
|
35
43
|
"trackly_get_apply_execution_snapshot": {
|
|
36
44
|
"officeProjectionMemberIdsMustExistInMemberIds": true
|
|
45
|
+
},
|
|
46
|
+
"trackly_advance_apply_execution": {
|
|
47
|
+
"accessReviewApprovalJobIdsMustBeUnique": true,
|
|
48
|
+
"accessReviewApprovalRequiresClearedPersonalDeferment": true,
|
|
49
|
+
"sameKeyReplayReturnsIdenticalProposedWaveAndRationale": true,
|
|
50
|
+
"proposedWaveIncludesFrozenIdentity": true,
|
|
51
|
+
"approvalReceiptBindsFrozenIdentity": true
|
|
52
|
+
},
|
|
53
|
+
"trackly_defer_apply_access": {
|
|
54
|
+
"serverDerivesIdentityFromJobId": true,
|
|
55
|
+
"agentsCannotSubmitUrlsOrRawChat": true,
|
|
56
|
+
"providerScopeAppliesAcrossCompanies": true
|
|
57
|
+
},
|
|
58
|
+
"trackly_clear_apply_access_deferment": {
|
|
59
|
+
"clearRequiresDiscoveredDefermentId": true,
|
|
60
|
+
"idempotentReplayPreserved": true,
|
|
61
|
+
"clearReceiptIncludesClearedAt": true,
|
|
62
|
+
"clearReceiptPersistsUntilClearedFalse": true
|
|
37
63
|
}
|
|
38
64
|
},
|
|
39
65
|
"schemaDigests": {
|
|
@@ -56,7 +82,10 @@
|
|
|
56
82
|
"trackly_get_apply_execution_snapshot": "{executionId:z.number().int().min(1),memberIds:z.array(z.number().int().min(1)).min(1).max(APPLY_EXECUTION_MAX_TARGET).refine((values)=>newSet(values).size===values.length,{message:'memberIdsmustbeunique'}),profileKeys:z.array(z.string().min(1).max(200)).max(100).refine((values)=>newSet(values).size===values.length,{message:'profileKeysmustbeunique'}).optional(),officeProjections:z.array(z.object({memberId:z.number().int().min(1),office:officeScopeSchema,profileKeys:z.array(z.string().min(1).max(200)).min(1).max(100).refine((values)=>newSet(values).size===values.length,{message:'officeprofileKeysmustbeunique'})}).strict()).max(APPLY_EXECUTION_MAX_TARGET).refine((values)=>newSet(values.map(({memberId})=>memberId)).size===values.length,{message:'officeProjectionsmustcontainuniquememberIdvalues'}).optional(),browserSurface:z.enum(APPLY_BROWSER_SURFACES)}",
|
|
57
83
|
"trackly_resume_parked_apply_member": "{executionId:z.number().int().min(1),memberId:z.number().int().min(1),expectedRevision:z.number().int().min(1),browserSurface:z.enum(APPLY_BROWSER_SURFACES),explicitUserResume:z.literal(true),idempotencyKey:z.string().min(16).max(200).regex(SAFE_IDEMPOTENCY_KEY)}",
|
|
58
84
|
"trackly_approve_apply_execution_resume": "{executionId:z.number().int().min(1),expectedRevision:z.number().int().min(1),originalSnapshotHash:z.string().regex(/^[a-f0-9]{64}$/),profileRevision:z.number().int().min(1),resumeId:z.number().int().min(1),resumeSha256:z.string().regex(/^[a-f0-9]{64}$/),resumeFilename:z.string().min(1).max(255),resumeSizeBytes:z.number().int().min(1),expiresAt:z.string().datetime(),idempotencyKey:z.string().min(16).max(200).regex(SAFE_IDEMPOTENCY_KEY)}",
|
|
59
|
-
"trackly_advance_apply_execution": "{executionId:z.number().int().min(1),expectedRevision:z.number().int().min(1),browserSurface:z.enum(APPLY_BROWSER_SURFACES),idempotencyKey:z.string().min(16).max(200).regex(SAFE_IDEMPOTENCY_KEY)}",
|
|
85
|
+
"trackly_advance_apply_execution": "{executionId:z.number().int().min(1),expectedRevision:z.number().int().min(1),browserSurface:z.enum(APPLY_BROWSER_SURFACES),idempotencyKey:z.string().min(16).max(200).regex(SAFE_IDEMPOTENCY_KEY),accessReviewApproval:z.object({jobIds:z.array(z.number().int().min(1)).min(1).max(APPLY_EXECUTION_MAX_TARGET).refine((values)=>newSet(values).size===values.length,{message:'jobIdsmustbeunique'}),approvalHash:z.string().regex(/^[a-f0-9]{64}$/)}).strict().optional()}",
|
|
86
|
+
"trackly_list_apply_access_deferments": "{}",
|
|
87
|
+
"trackly_defer_apply_access": "{jobId:z.number().int().min(1),scope:z.enum(APPLY_ACCESS_DEFERMENT_SCOPES),idempotencyKey:z.string().min(16).max(200).regex(SAFE_IDEMPOTENCY_KEY)}",
|
|
88
|
+
"trackly_clear_apply_access_deferment": "{defermentId:z.number().int().min(1),idempotencyKey:z.string().min(16).max(200).regex(SAFE_IDEMPOTENCY_KEY)}",
|
|
60
89
|
"trackly_record_apply_execution_dispositions": "{executionId:z.number().int().min(1),expectedRevision:z.number().int().min(1),idempotencyKey:z.string().min(16).max(200).regex(SAFE_IDEMPOTENCY_KEY),dispositions:z.array(applyExecutionDispositionSchema).min(1).max(APPLY_EXECUTION_MAX_TARGET)}",
|
|
61
90
|
"trackly_stop_apply_execution": "{executionId:z.number().int().min(1),expectedRevision:z.number().int().min(1),idempotencyKey:z.string().min(16).max(200).regex(SAFE_IDEMPOTENCY_KEY),reasonCode:z.enum(APPLY_EXECUTION_STOP_REASON_CODES).optional()}",
|
|
62
91
|
"trackly_create_apply_batch": "{limit:z.number().int().min(1).max(100),idempotencyKey:z.string().min(16).max(200).regex(SAFE_IDEMPOTENCY_KEY)}",
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Bind access-review approval to validated proposal receipts
|
|
3
|
+
date: 2026-09-05
|
|
4
|
+
last_updated: 2026-09-05
|
|
5
|
+
category: integration-issues
|
|
6
|
+
module: Trackly Apply access-review facade
|
|
7
|
+
problem_type: integration_issue
|
|
8
|
+
component: mcp_facade
|
|
9
|
+
symptoms:
|
|
10
|
+
- A compact backend proposal could be rejected because the client required display identity fields in both projections
|
|
11
|
+
- Active execution recovery could reject the backend's enabled metadata or expose access_review without its proposal
|
|
12
|
+
- An empty access-review proposal could pass the client response schema and reach approval handling
|
|
13
|
+
- Follow-up proposal caching and key-order-sensitive accessKnowledge comparisons could reject a valid continuation
|
|
14
|
+
- The deferment list reused the execution-wave cap instead of naming the backend's active-deferment limit
|
|
15
|
+
root_cause: missing_validation
|
|
16
|
+
resolution_type: code_fix
|
|
17
|
+
severity: high
|
|
18
|
+
related_components: [api_layer, contract_verifier, documentation]
|
|
19
|
+
tags: [trackly-apply, access-review, proposal-receipt, zod, hosted-contract, fail-closed]
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
# Bind access-review approval to validated proposal receipts
|
|
23
|
+
|
|
24
|
+
## Problem
|
|
25
|
+
|
|
26
|
+
The CLI receives an immutable access-review proposal before it can advance an
|
|
27
|
+
Apply execution. Approval must remain bound to that exact server receipt across
|
|
28
|
+
the response validator, the local cache, the hosted contract verifier, and the
|
|
29
|
+
agent-facing skill instructions.
|
|
30
|
+
|
|
31
|
+
## Symptoms
|
|
32
|
+
|
|
33
|
+
The deployed 3.8.x backend uses a compact local `proposedWave` array and keeps
|
|
34
|
+
the rich approval receipt in `accessProposal.members`. The client previously
|
|
35
|
+
required company, title, provider, and URL fields in both projections, so a real
|
|
36
|
+
response failed before approval. The active endpoint also returns `enabled`,
|
|
37
|
+
`active`, and `preserved`, while a pending access review is hydrated by the
|
|
38
|
+
detail endpoint rather than inlined. The response schemas accepted an empty
|
|
39
|
+
`proposedWave` or `accessProposal` without checking its counts. An access-review
|
|
40
|
+
continuation normally requires a concrete member set, while the all-deferred
|
|
41
|
+
case legitimately has no members and must be recognized by zero available
|
|
42
|
+
candidates plus a positive deferred count. Exact recovery can also be blocked
|
|
43
|
+
by a user deferment with zero members even when available candidates remain;
|
|
44
|
+
that `recovery_blocked_by_user_deferment` rationale is a safe stop/clear path.
|
|
45
|
+
Older 3.8.0 responses may omit the
|
|
46
|
+
optional deferment mapping, so those receipts remain a safe stop/expiry state
|
|
47
|
+
until a refreshed response supplies exact IDs. After an approval returned another
|
|
48
|
+
`nextAction: access_review`, the new proposal was not cached, so the next exact
|
|
49
|
+
approval was rejected locally. Matching nested `accessKnowledge` values by raw
|
|
50
|
+
JSON text also made harmless object key order look like a changed receipt.
|
|
51
|
+
Finally, the hosted verifier selected runtime class members before rejecting
|
|
52
|
+
computed keys, allowing a computed member to shadow the reviewed method without
|
|
53
|
+
being inspected.
|
|
54
|
+
|
|
55
|
+
## Root cause
|
|
56
|
+
|
|
57
|
+
The implementation treated each response branch independently. Schema presence
|
|
58
|
+
checks did not encode nonempty review membership or compact-vs-rich projection
|
|
59
|
+
parity, active recovery did not account for the endpoint's two-step shape, the
|
|
60
|
+
approval cache handled only the first proposal, and identity comparison relied
|
|
61
|
+
on serialization order. The verifier's member lookup likewise happened before
|
|
62
|
+
its computed-key guard.
|
|
63
|
+
|
|
64
|
+
## What Didn't Work
|
|
65
|
+
|
|
66
|
+
A single proposal-response model could not safely cover start, per-execution
|
|
67
|
+
reads, active recovery, and advance because those endpoints return different
|
|
68
|
+
identity and metadata envelopes. Passing responses through raw exposed
|
|
69
|
+
unmodelled fields, while forcing every active response through the access-review
|
|
70
|
+
schema rejected ordinary and preserved-terminal recovery. Caching only the
|
|
71
|
+
first proposal, clearing replay state on every refresh, and leaving proposal
|
|
72
|
+
bindings unbounded broke either exact retry behavior or long-running process
|
|
73
|
+
memory safety. Raw JSON serialization made receipt equality depend on object-key
|
|
74
|
+
order. Reusing the execution-wave limit for deferment discovery happened to
|
|
75
|
+
match the backend's current limit of 20, but concealed a separate contract that
|
|
76
|
+
could drift independently.
|
|
77
|
+
|
|
78
|
+
## Solution
|
|
79
|
+
|
|
80
|
+
- Accept the deployed compact local projection and the optional complete future
|
|
81
|
+
display identity projection, while keeping the rich receipt's immutable
|
|
82
|
+
fields required and matching both projections by ordered job ID and
|
|
83
|
+
`accessKnowledge`.
|
|
84
|
+
- Strictly validate start, advance, and active execution envelopes, preserve
|
|
85
|
+
`enabled`/`active`/`preserved`, and hydrate a pending access review from the
|
|
86
|
+
detail endpoint only when execution revision and `nextAction` still match.
|
|
87
|
+
- Require a nonempty member set for normal access-review proposals, while
|
|
88
|
+
accepting the bounded all-deferred shape with zero available candidates and at
|
|
89
|
+
least one deferred candidate. Also accept an empty
|
|
90
|
+
`recovery_blocked_by_user_deferment` receipt when at least one candidate is
|
|
91
|
+
deferred. Validate optional deferment mappings when supplied; a legacy
|
|
92
|
+
response without mappings cannot authorize a clear action.
|
|
93
|
+
- Validate progress candidate counts against the access proposal counts while
|
|
94
|
+
the response is still an `access_review`; after approval, the proposal is a
|
|
95
|
+
historical receipt and progress is allowed to reflect the newly created wave.
|
|
96
|
+
- Canonicalize nested `accessKnowledge` objects by key before comparing the
|
|
97
|
+
simple and rich proposal representations.
|
|
98
|
+
- Cache a newly returned access-review proposal after a successful approval so
|
|
99
|
+
the next continuation uses its current revision, ordered IDs, and hash. Bound
|
|
100
|
+
pending and replayable proposal bindings with oldest-entry eviction so an
|
|
101
|
+
abandoned execution cannot retain process memory indefinitely.
|
|
102
|
+
- Reject all computed instance members before selecting the hosted runtime
|
|
103
|
+
method, with fixtures for computed methods and fields.
|
|
104
|
+
- Keep the local and adapted Apply skills explicit about
|
|
105
|
+
`accessProposal.approvalHash`, and distinguish creation `jobId` values from
|
|
106
|
+
the discovered `defermentId` required to clear a deferment.
|
|
107
|
+
- Validate deferment discovery against the backend's dedicated active-deferment
|
|
108
|
+
limit rather than coupling it to the numerically equal execution-wave limit.
|
|
109
|
+
|
|
110
|
+
## Why This Works
|
|
111
|
+
|
|
112
|
+
Each endpoint is parsed through its own strict envelope before its response is
|
|
113
|
+
returned or cached. An access-review continuation must contain both the compact
|
|
114
|
+
proposal and rich receipt, and the validator binds their ordered job IDs,
|
|
115
|
+
contiguous member positions, frozen access knowledge, counts, and approval hash.
|
|
116
|
+
Start and active recovery hydrate the detail endpoint only when execution ID,
|
|
117
|
+
revision, and next action still match, preventing a stale detail response from
|
|
118
|
+
authorizing work. Separate bounded pending and replay caches preserve exact
|
|
119
|
+
same-key idempotency while rejecting a new key or evicting abandoned execution
|
|
120
|
+
state. Canonical object comparison removes serialization-order dependence, and
|
|
121
|
+
the separately named deferment limit prevents unrelated contract constants from
|
|
122
|
+
being coupled accidentally.
|
|
123
|
+
|
|
124
|
+
## Verification
|
|
125
|
+
|
|
126
|
+
PR #135 merged as `e97309a07a8afdc6eb62b28f4313c538699c72e5`.
|
|
127
|
+
Its CI passed 520 tests and the checked-in contract-fixture run passed 81 tests.
|
|
128
|
+
|
|
129
|
+
Backend PR `#1769` merged as `254e1c6fc3f98e2f3aa8c6492702e9b9480ca22e`
|
|
130
|
+
(head `4c4dab848b1068b1bd9290b14f8d278ec643aca6`) on 2026-09-05 14:00 PDT.
|
|
131
|
+
Production `https://closeai.mba/api/first-test` reports `build_sha`
|
|
132
|
+
`d5a9abf3e12fb384d50ec998d83a2370725ae3e7`, which contains that merge, and
|
|
133
|
+
every hosted path the CLI locks is byte-identical between the two commits.
|
|
134
|
+
Protected migration 507 completed on 2026-09-06 07:13 PDT
|
|
135
|
+
(`providerDefermentsReady: true` on an idempotent rerun after the first request
|
|
136
|
+
outlived the Cloudflare proxy timeout). The hosted contract fixture was
|
|
137
|
+
recaptured from `254e1c6fc3`, and
|
|
138
|
+
`TRACKLY_BACKEND_DIR=<clean checkout at 254e1c6fc3> npm run test:hosted-contract`
|
|
139
|
+
passes with Apply 3.8.1 and the 21-tool public facade at 1.0.0.
|
|
140
|
+
|
|
141
|
+
Still pending: npm publication of 0.18.1 through the merge of PR #136, and the
|
|
142
|
+
backend rollout switch `TRACKLY_APPLY_ACCESS_KNOWLEDGE_ROLLOUT`, which is unset
|
|
143
|
+
in production, so the live protocol still advertises the 3.7.x window; that
|
|
144
|
+
activation is a backend rollout decision outside this repository.
|
|
145
|
+
|
|
146
|
+
## Prevention
|
|
147
|
+
|
|
148
|
+
When an access-review response shape changes, update the executed schema,
|
|
149
|
+
response cache lifecycle, identity comparison, hosted verifier, both skill
|
|
150
|
+
surfaces, and regression fixtures together. Treat server-provided proposal
|
|
151
|
+
identity and hashes as the only approval authority.
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Keep destructive confirmation guards at the mutation boundary
|
|
3
|
+
date: 2026-08-28
|
|
4
|
+
category: integration-issues
|
|
5
|
+
module: Trackly Apply sensitive revocation
|
|
6
|
+
problem_type: security_issue
|
|
7
|
+
component: service_layer
|
|
8
|
+
symptoms:
|
|
9
|
+
- A stale trackly-cli 0.10.0 MCP server could revoke sensitive-storage consent without presenting the new confirmation challenge
|
|
10
|
+
- The backend accepted an unconfirmed destructive request because confirmation was enforced only by caller-side tool handlers
|
|
11
|
+
- The bypass deleted 33 encrypted application-profile answers before a recovery archive existed
|
|
12
|
+
root_cause: missing_validation
|
|
13
|
+
resolution_type: code_fix
|
|
14
|
+
severity: critical
|
|
15
|
+
related_components: [assistant, api_layer]
|
|
16
|
+
tags: [trackly-apply, sensitive-revocation, destructive-action, version-skew, backend-authority, mcp, data-loss]
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# Keep destructive confirmation guards at the mutation boundary
|
|
20
|
+
|
|
21
|
+
## Problem
|
|
22
|
+
|
|
23
|
+
Sensitive-answer deletion originally used a two-step confirmation guard in the hosted and local MCP callers. That protected only clients running the new code. During verification, a stale local MCP server running trackly-cli 0.10.0 sent an unconfirmed revocation request directly to the backend, which accepted it and permanently deleted 33 encrypted answers before a recovery archive existed.
|
|
24
|
+
|
|
25
|
+
This was a trust-boundary failure, not merely a missed client upgrade. Any old, alternate, or defective caller could omit a caller-owned safety check while still reaching the destructive service operation.
|
|
26
|
+
|
|
27
|
+
## Symptoms
|
|
28
|
+
|
|
29
|
+
- A destructive request from an older installed client bypassed the newly shipped caller-side guard. The incident transcript records the resulting loss of 33 encrypted answers; [trackly-cli PR #90](https://github.com/trackly-app/trackly-cli/pull/90) independently records stale CLI 0.10.0 as the failure mode.
|
|
30
|
+
- The superseded implementation in [trackly-cli PR #89](https://github.com/trackly-app/trackly-cli/pull/89) computed the affected-key inventory and confirmation token in the client before sending the mutation.
|
|
31
|
+
- Review found that a deterministic caller-computed token was an interaction speed bump, not a server-enforced consent boundary: an automated caller could derive or echo it without proving that a person saw the affected keys.
|
|
32
|
+
|
|
33
|
+
## What Didn't Work
|
|
34
|
+
|
|
35
|
+
The first implementation mirrored safety logic into local and hosted MCP handlers. The client recognized a revocation, fetched a profile, computed a challenge, and withheld the API request until the supplied token matched.
|
|
36
|
+
|
|
37
|
+
That implementation could be thoroughly tested and still fail globally. A previously installed client had neither the preflight read nor the challenge branch, and the backend could not distinguish its unconfirmed request from an approved one. Duplicating the guard also forced callers to mirror the deletion predicate, token rules, revision behavior, and error shape. Those copies could drift independently from the mutation they were meant to protect.
|
|
38
|
+
|
|
39
|
+
Adding more checks to every caller would not close the version-skew hole. Compatibility checks may make obsolete clients inconvenient to use, but they are not an authorization boundary for a reachable backend endpoint.
|
|
40
|
+
|
|
41
|
+
## Solution
|
|
42
|
+
|
|
43
|
+
Move the mandatory challenge-and-confirm invariant to the backend operation that performs the deletion, then keep MCP callers as thin protocol adapters. The backend implementation is outside this repository; internal/private [close-ai PR #1296](https://github.com/trackly-app/close-ai/pull/1296) and public [trackly-cli PR #90](https://github.com/trackly-app/trackly-cli/pull/90) record the service-authoritative design.
|
|
44
|
+
|
|
45
|
+
The current CLI publishes `sensitiveRevocationConfirmToken` in the tool schema and forwards the complete request to the application-profile endpoint:
|
|
46
|
+
|
|
47
|
+
```js
|
|
48
|
+
wrapTool(async (params) => {
|
|
49
|
+
return applyApiRequest(
|
|
50
|
+
'PATCH',
|
|
51
|
+
'/api/jobscout/application-profile',
|
|
52
|
+
params,
|
|
53
|
+
false,
|
|
54
|
+
false,
|
|
55
|
+
MCP_USER_AGENT,
|
|
56
|
+
);
|
|
57
|
+
}, 'Failed to update application profile')
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The handler does not compute, strip, or validate its own copy of the guard (`mcp/apply-tools.js`). The MCP error mapper preserves the service's structured HTTP 409 challenge (`mcp/server.js`). The regression test in `test/mcp-schema.test.js` verifies that the CLI publishes the token field, relays the service challenge, forwards the confirmed token, and leaves ordinary opt-in as a single service round trip.
|
|
61
|
+
|
|
62
|
+
PRs #89 and #90 are both merged into the default branch. PR #89 is important history because it demonstrates why the caller-owned design was insufficient; PR #90 is the corrective CLI design.
|
|
63
|
+
|
|
64
|
+
## Why This Works
|
|
65
|
+
|
|
66
|
+
Every backend-classified agent caller, including stale CLI and MCP versions, must cross the backend mutation boundary. A stale agent client may have a worse experience or be unable to complete a newer flow, but it cannot make the backend omit its own destructive-operation check. First-party UI clients intentionally retain their own single-call confirmation flows.
|
|
67
|
+
|
|
68
|
+
The separation also removes semantic duplication. The backend owns the authoritative deletion decision, confirmation validity, revision binding, archive behavior, and mutation. MCP owns discoverability and transport: publish the token field, explain the two-step interaction, preserve the full request, and relay the service response.
|
|
69
|
+
|
|
70
|
+
Internal/private [close-ai issue #1314](https://github.com/trackly-app/close-ai/issues/1314) remains administratively open, but current backend main appears to satisfy its scoped-inventory acceptance criteria through merged internal/private [close-ai PR #1369](https://github.com/trackly-app/close-ai/pull/1369): the service inventories sensitive and restricted rows across scopes, serializes scope identity into `affectedKeys`, and uses the same user-and-sensitivity predicate for archival and deletion. Close the issue only after confirming the fix is deployed and behaviorally verified; an open tracker alone is not evidence of a current functional gap.
|
|
71
|
+
|
|
72
|
+
## Prevention
|
|
73
|
+
|
|
74
|
+
- Treat destructive confirmation as a backend invariant and enforce it in the authoritative operation or transaction that changes the data.
|
|
75
|
+
- Keep first-party dialogs and MCP challenge descriptions for user understanding, but never count them as the only protection.
|
|
76
|
+
- Make callers forward the full confirmed request, and test the exact service-bound body.
|
|
77
|
+
- Relay authoritative conflict payloads without regenerating the decision in the caller.
|
|
78
|
+
- Test across the real trust boundary. A caller-only test proves the newest caller behaves correctly; it cannot prove an older caller is safe.
|
|
79
|
+
- During coordinated releases, deploy and verify the server guard before removing caller-side defense in depth.
|
|
80
|
+
- For every destructive control, ask what happens when the request comes from last month's client. If safety depends on that client running new code, the invariant is in the wrong layer.
|
|
81
|
+
|
|
82
|
+
## Related Issues
|
|
83
|
+
|
|
84
|
+
- [trackly-cli PR #89](https://github.com/trackly-app/trackly-cli/pull/89) — initial caller-owned guard, retained as the superseded attempt
|
|
85
|
+
- [trackly-cli PR #90](https://github.com/trackly-app/trackly-cli/pull/90) — thin caller relay and service-authoritative correction
|
|
86
|
+
- Internal/private [close-ai PR #1296](https://github.com/trackly-app/close-ai/pull/1296) — backend enforcement recorded by the coordinated fix
|
|
87
|
+
- Internal/private [close-ai issue #1314](https://github.com/trackly-app/close-ai/issues/1314) and [close-ai PR #1369](https://github.com/trackly-app/close-ai/pull/1369) — tracker still open; current main contains the scoped-inventory fix pending deployment/behavior confirmation
|
|
88
|
+
- [Durable exact recovery after browser state loss](durable-exact-recovery-after-browser-state-loss.md) — related Trackly Apply authority-boundary learning
|
package/docs/trackly-tools.md
CHANGED
|
@@ -115,7 +115,10 @@ API-key creation attempts.
|
|
|
115
115
|
- **trackly_get_apply_execution_snapshot**: Fetch a compact bounded projection of current members, requested profile keys, mutability, allowed operations, milestones, lease timing, and the authoritative funnel.
|
|
116
116
|
- **trackly_resume_parked_apply_member**: Resume one parked member only after explicit user instruction; a fresh non-mutating access probe remains required before form mutation.
|
|
117
117
|
- **trackly_approve_apply_execution_resume**: Approve one exact resume identity for an execution's unchanged original snapshot while preserving immediate per-run local verification before upload.
|
|
118
|
-
- **trackly_advance_apply_execution** — Transactionally select the next wave from the execution's original recent-first snapshot for the declared `browserSurface`. Same-key retries return
|
|
118
|
+
- **trackly_advance_apply_execution** — Transactionally select the next wave from the execution's original recent-first snapshot for the declared `browserSurface`. Returns a compact immutable `proposedWave` with each member's job ID and value-free `accessKnowledge`, plus the rich `accessProposal` receipt with member order, rationale, and approval hash. Same-key retries return identical identity, order, and rationale. Optional hash-bound `accessReviewApproval` probes exact proposed job IDs after personal deferments are cleared.
|
|
119
|
+
- **trackly_list_apply_access_deferments** — List the current user's persistent job/company/provider access deferments. Returns only scope identities; never provider names, URLs, or raw text.
|
|
120
|
+
- **trackly_defer_apply_access** — Persist an explicit user deferment for one Trackly `jobId` at `job`, `company`, or `provider` scope. The server derives company, provider, tenant, origin, and route; provider scope applies across companies until cleared; never submit a provider name, URL, or raw text.
|
|
121
|
+
- **trackly_clear_apply_access_deferment** — Clear one deferment listed or created in the current MCP session. Exact same-session replay is idempotent; after restart, refresh the active list instead of replaying an undiscovered cleared ID.
|
|
119
122
|
- **trackly_record_apply_execution_dispositions** — Record typed, value-free live-probe classifications for the current wave. Every item requires `jobId`, one allowed `classification`, `source: 'live_probe'`, and the exact `batchId`, `memberId`, `runId`, `expectedMemberVersion`, `expectedInspectionEpoch`, and `browserSurface`; cache/static scheduling records are server-owned and cannot be submitted through MCP.
|
|
120
123
|
- **trackly_stop_apply_execution** — Stop an execution with optimistic revision and idempotency guards.
|
|
121
124
|
- **trackly_create_apply_batch** — Freeze an exact recent-first set of approved jobs with an idempotency key.
|
|
@@ -145,6 +148,12 @@ API-key creation attempts.
|
|
|
145
148
|
|
|
146
149
|
Apply contract v3 intentionally gives this verifier different local and hosted schemas: local MCP receives the full proof needed to inspect the private file, while hosted MCP accepts only run and confirmation identifiers and returns the manual/local-agent requirement. Local paths are never sent remotely. Resume fingerprints are sent only to authenticated Trackly resume approval and truth-certification endpoints, never observations or employer forms. Version 3.1 also records universal value-free evidence for critical-contact integrity and the manual-submit boundary. Version 3.2 authorizes the exact stored HTTPS origin for jobs Trackly ingested from employer careers sources, without granting redirect, iframe, or hostname-suffix privileges. Version 3.3.1 adds active-batch recovery, epoch-bound observations/outcomes, and truth certification for forms with no resume control. Version 3.4 adds server-owned accessible executions above immutable child batches, typed access dispositions, and an authoritative target-completion funnel.
|
|
147
150
|
|
|
151
|
+
Version 3.8.1 adds access-aware scheduling receipts, `nextAction: access_review`,
|
|
152
|
+
hash-bound probe approval, and job/company/provider deferment tools. Provider scope
|
|
153
|
+
applies across companies until explicitly cleared. Historical OPEN never
|
|
154
|
+
authorizes form filling; every selected job still requires a fresh non-mutating
|
|
155
|
+
live probe.
|
|
156
|
+
|
|
148
157
|
Version 3.7 adds public exact-member recovery and scoped review-handoff claims,
|
|
149
158
|
plus local tab-finalizer and resume-upload proof validators. Recovery candidates
|
|
150
159
|
are bounded and value-free. The local helpers perform no browser actions and
|
package/lib/agent.js
CHANGED
|
@@ -8,13 +8,13 @@ const { spawnSync } = require('node:child_process');
|
|
|
8
8
|
const { apiRequest, downloadFile, getConfigPaths, hasAuth, maintenanceOutput } = require('./client');
|
|
9
9
|
const { version: PACKAGE_VERSION } = require('../package.json');
|
|
10
10
|
const { contractVersion: MCP_CONTRACT_VERSION } = require('../contracts/trackly-apply-tools.json');
|
|
11
|
-
const SKILL_VERSION = '4.
|
|
11
|
+
const SKILL_VERSION = '4.8.0';
|
|
12
12
|
const CLI_USER_AGENT = `trackly-cli/${PACKAGE_VERSION}`;
|
|
13
13
|
const MCP_USER_AGENT = `trackly-mcp/${PACKAGE_VERSION}`;
|
|
14
14
|
|
|
15
15
|
const SKILL_NAME = 'trackly-apply';
|
|
16
16
|
const SKILL_MAJOR = Number(SKILL_VERSION.split('.')[0]);
|
|
17
|
-
const MIN_APPLY_PROTOCOL_VERSION = '3.
|
|
17
|
+
const MIN_APPLY_PROTOCOL_VERSION = '3.7.0';
|
|
18
18
|
const CACHE_TTL_MS = 2 * 60 * 60 * 1000;
|
|
19
19
|
const MANAGED_FILE = '.trackly-managed.json';
|
|
20
20
|
const SKILL_MANIFEST_VERSION = 1;
|