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 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 | 55 local MCP tools
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 (55 local tools) | -- | -- |
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 55 tools: the complete Trackly
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.7.6",
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
@@ -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 current authoritative progress and the current execution revision.
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.7.1';
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.6.0';
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;