Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
b385a87
Begin Handshake CLI checkpoint
isomorphisms Sep 3, 2026
d794904
Implement Handshake offline surfaces and isolate transport gap
isomorphisms Sep 3, 2026
7175b42
Document resolved Handshake surfaces and true transport dependency
isomorphisms Sep 3, 2026
793f63a
Exercise Handshake offline checkpoint end to end
isomorphisms Sep 3, 2026
545d986
Add current-Idric Handshake acceptance workflow
isomorphisms Sep 3, 2026
ffff423
Use Idric contrib JSON surface in Handshake checkpoint
isomorphisms Sep 3, 2026
e017ef2
Expose current Idric contrib modules to Handshake check
isomorphisms Sep 3, 2026
8f842f6
Document existing Idric JSON surface for Handshake
isomorphisms Sep 3, 2026
a3fbc21
Check Handshake from an isolated module root
isomorphisms Sep 3, 2026
4468265
Name the genuine Handshake ICU credential gap
isomorphisms Sep 3, 2026
3997cbe
Exercise Handshake TSV field cleanup in fixture
isomorphisms Sep 3, 2026
4c47032
Document Handshake ICU credential-policy gap precisely
isomorphisms Sep 3, 2026
5ce16a9
Clarify Handshake's remaining gap in root README
isomorphisms Sep 3, 2026
59f613e
Normalize Handshake case branch layout for current Idric
isomorphisms Sep 3, 2026
4a87c8e
Use ICU credential-safe GET for Handshake
isomorphisms Sep 4, 2026
eaf02dc
Keep Handshake source mirror in sync
isomorphisms Sep 4, 2026
2b7914b
Exercise Handshake through ICU redirect policy
isomorphisms Sep 4, 2026
1a3ca20
Run Handshake against ICU credential redirect stack
isomorphisms Sep 4, 2026
e1088c8
Preserve Handshake source symlink
isomorphisms Sep 4, 2026
24778b2
Update Handshake transport status
isomorphisms Sep 4, 2026
4708a6f
Document credential-safe Handshake transport
isomorphisms Sep 4, 2026
83b29ef
Record Handshake transport acceptance heads
isomorphisms Sep 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 135 additions & 0 deletions .github/workflows/handshake-idric.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
name: Handshake Idric checkpoint

on:
pull_request:
paths:
- 'Handshake.idric'
- 'checkpoints/handshake/**'
- '.github/workflows/handshake-idric.yml'
workflow_dispatch:

permissions:
contents: read

jobs:
handshake:
runs-on: ubuntu-latest
steps:
- name: Checkout idric-cli
uses: actions/checkout@v4

- name: Checkout current Idric
uses: actions/checkout@v4
with:
repository: isomorphisms/Idric
ref: Idriç
path: .tools/Idric

- name: Checkout ICU credential-header stack
uses: actions/checkout@v4
with:
repository: dilapidated-shed/icu
ref: credential-sensitive-headers
path: .tools/icu

- name: Record tested source heads
run: |
printf 'Idric\t%s\n' "$(git -C "$GITHUB_WORKSPACE/.tools/Idric" rev-parse HEAD)"
printf 'ICU\t%s\n' "$(git -C "$GITHUB_WORKSPACE/.tools/icu" rev-parse HEAD)"

- name: Install host build tools
run: |
sudo apt-get update
sudo apt-get install -y build-essential curl libssl-dev

- name: Bootstrap Idric
run: .tools/Idric/_/edric bootstrap

- name: Expose pinned Chez
run: echo "$GITHUB_WORKSPACE/.tools/Idric/_/.tools/bin" >> "$GITHUB_PATH"

- name: Build and check ICU transport
env:
IDRIS2_PREFIX: ${{ github.workspace }}/.tools/Idric/_/bootstrap-build
run: |
make -C "$GITHUB_WORKSPACE/.tools/icu" check-native
make -C "$GITHUB_WORKSPACE/.tools/icu" \
IDRIC="$GITHUB_WORKSPACE/.tools/Idric/_/build/exec/idris2"

- name: Start deterministic credential redirect servers
env:
HANDSHAKE_JOBS_FIXTURE: ${{ github.workspace }}/checkpoints/handshake/fixture/jobs.json
run: |
cat > "$RUNNER_TEMP/handshake-redirect.py" <<'PY'
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
import os
import threading

fixture = Path(os.environ["HANDSHAKE_JOBS_FIXTURE"]).read_bytes()

class FirstOrigin(BaseHTTPRequestHandler):
protocol_version = "HTTP/1.0"

def log_message(self, format, *args):
pass

def redirect(self, location):
self.send_response(302)
self.send_header("Location", location)
self.send_header("Content-Length", "0")
self.end_headers()

def do_GET(self):
if self.headers.get("x-api-key") != "synthetic":
self.send_error(400, "x-api-key missing on same-origin hop")
return
if self.path == "/v1/jobs?page_size=100":
self.redirect("/v1/jobs/continued")
return
if self.path == "/v1/jobs/continued":
self.redirect("http://127.0.0.1:18084/final")
return
self.send_error(404)

class SecondOrigin(BaseHTTPRequestHandler):
protocol_version = "HTTP/1.0"

def log_message(self, format, *args):
pass

def do_GET(self):
if self.path != "/final":
self.send_error(404)
return
if self.headers.get("x-api-key") is not None:
self.send_error(400, "x-api-key leaked across origin")
return
self.send_response(200)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(fixture)))
self.end_headers()
self.wfile.write(fixture)

first = ThreadingHTTPServer(("127.0.0.1", 18083), FirstOrigin)
second = ThreadingHTTPServer(("127.0.0.1", 18084), SecondOrigin)
threading.Thread(target=first.serve_forever, daemon=True).start()
second.serve_forever()
PY
python3 "$RUNNER_TEMP/handshake-redirect.py" \
> "$RUNNER_TEMP/handshake-redirect.log" 2>&1 &
echo $! > "$RUNNER_TEMP/handshake-redirect.pid"
sleep 1

- name: Run Handshake checkpoint
env:
IDRIC: ${{ github.workspace }}/.tools/Idric/_/build/exec/idris2
IDRIS2_PREFIX: ${{ github.workspace }}/.tools/Idric/_/bootstrap-build
IDRIS2_PATH: ${{ github.workspace }}/.tools/Idric/_/libs/contrib/build/ttc:${{ github.workspace }}/.tools/icu/build/ttc
LD_LIBRARY_PATH: ${{ github.workspace }}/.tools/icu
HANDSHAKE_TEST_EDU_BASE_URL: http://127.0.0.1:18083/v1
run: bash checkpoints/handshake/check

- name: Show redirect server log on failure
if: failure()
run: cat "$RUNNER_TEMP/handshake-redirect.log" 2>/dev/null || true
1 change: 1 addition & 0 deletions Handshake.idric
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ This repository is the consolidation point for the CLI/API-access programs that
- `Economist.idric` — Economist API checkpoint.
- `Ft.idric` — Financial Times API checkpoint.
- `Guardian.idric` — Guardian API checkpoint.
- `Handshake.idric` — Handshake EDU API checkpoint for jobs and job-role classifications, plus public job URLs.
- `Nyt.idric` — New York Times API checkpoint.
- `Reddit.idric` — Reddit Data API checkpoint, with a synthetic fixture and manual receipt.
- `Reuters.idric` — Reuters GraphQL checkpoint.
Expand All @@ -23,10 +24,10 @@ Some Idriç clients intentionally contain named holes for compiler/library bound

## Networking

Where these clients need networking, ICU/Idric-Net remains the intended transport boundary. ICU is deliberately **not** a Git submodule here and is not pinned by this repository.
Where these clients need networking, ICU/Idric-Net remains the intended transport boundary. ICU is deliberately **not** a Git submodule here. While ICU's caller-header/capture stack is still pending, the Handshake compiler checkpoint checks out that current stack explicitly so its transport dependency is executable rather than replaced by curl or Python.

## Tests

`make test` runs the existing Amazon and AbeBooks smoke tests. Reddit has a separate manual compiler checkpoint at `checkpoints/reddit/check`; it is not part of `make test` while named Idriç holes remain.
`make test` runs the existing Amazon and AbeBooks smoke tests. Reddit and Handshake have separate compiler checkpoints under `checkpoints/`. Handshake now uses ICU's caller-declared credential-header surface from ICU #20: `x-api-key` is retained across same-origin redirects and stripped before a cross-origin request. The deterministic Handshake checkpoint exercises that redirect chain with a synthetic key and fixture response.

See `PROVENANCE.md` for the source branches copied into this repository.
131 changes: 131 additions & 0 deletions checkpoints/handshake/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# Handshake CLI compiler checkpoint

This directory is a Handshake EDU API command-line client checkpoint in Idriç.

The slice is deliberately narrow: use Handshake's documented read-only EDU API for `jobs` and `job_role_groups`, keep the API-key/header boundary explicit, and expose public job URLs separately. Do not treat Handshake's authenticated student web application as a public API and do not make this checkpoint green by replaying browser cookies or substituting curl/Python.

## Why these two endpoints first

The immediate analytical use is to compare what a posting actually says with the occupational/job-role classification Handshake attaches to it. The official EDU API documents:

- `/jobs` — job id, title, employer id, employment type, job type, pay rate, remote/on-site/hybrid flags, salary type, external URL, timestamps, and related fields;
- `/job_role_groups` — job id, Handshake job-role-group id, job-role-group name, and timestamps.

Keeping those tables separate preserves Handshake's own data model. A later slice can join them by `job_id` and measure classification disagreements without silently rewriting the source data.

Official documentation, checked 2026-09-02:

- https://support.joinhandshake.com/hc/en-us/articles/31061076506391-Getting-Started-with-EDU-API
- https://support.joinhandshake.com/hc/en-us/articles/35762729693719-EDU-API-Endpoint-Definitions

## Access boundary

Handshake describes the EDU API as read-only and institution-scoped. Access requires an EDU API subscription approved by Handshake Support and requests use an `x-api-key` header.

This checkpoint therefore expects:

```text
HANDSHAKE_EDU_API_KEY=...
```

That credential is not assumed to exist. Missing and empty credentials are explicit refusal cases.

The public job catalog is a separate surface. Public job detail pages currently use URLs such as:

```text
https://app.joinhandshake.com/public/jobs/JOB_ID
```

`handshake public JOB_ID` only constructs that public URL. It does not claim that the public catalog has the same API contract as the institution-scoped EDU API.

## Command contract

```text
handshake url jobs
handshake url roles
handshake public JOB_ID
handshake fixture-jobs FILE
handshake fixture-roles FILE
handshake jobs
handshake roles
```

### `url jobs` / `url roles`

No network. Print the documented EDU API endpoint with a page size of 100.

### `public JOB_ID`

No network. Validate a nonempty ASCII decimal Handshake job id and print its public job-detail URL.

### `fixture-jobs` / `fixture-roles`

No network. Decode a synthetic EDU-API-shaped JSON response and emit TSV.

Committed fixtures:

```text
fixture/jobs.json
fixture/jobs.expected.tsv
fixture/job_role_groups.json
fixture/job_role_groups.expected.tsv
```

The jobs fixture includes escaped tab/newline characters in a title while its expected TSV contains spaces, so the receipt exercises field cleanup as well as JSON decoding.

### `jobs` / `roles`

Read `HANDSHAKE_EDU_API_KEY`, build `x-api-key` with ICU's caller-declared credential-header surface, perform one ICU GET with file-backed response capture, decode the response, and emit the same TSV shape as the fixtures.

The credential tag does not change the header's wire representation. Its only policy effect is on redirects: same-origin redirects retain `x-api-key`; a change of scheme, host, or port strips it before the redirected request is sent.

This first slice intentionally does not paginate. Handshake documents cursor pagination (`next_cursor` / `page_cursor`); pagination should follow after a single live page is accepted.

`HANDSHAKE_EDU_BASE_URL` exists so deterministic acceptance can direct the live command at a local fixture server. The `url jobs` and `url roles` commands continue to report the documented production URLs.

## Surface audit

The original draft made seven named holes. They are now either implemented through current Idriç surfaces/small client-local logic or covered by the pending ICU stack:

| Original hole | Present surface | Current treatment |
| --- | --- | --- |
| TSV field cleanup | `String`, `pack`, `unpack` | local replacement of tab/newline/carriage-return with spaces |
| fixture input | `System.File.readFile : ... → Either FileError String` | implemented with explicit file-error text |
| jobs JSON decoding | `Language.JSON.parse`, `JSON`, `lookup` | typed `/jobs` decoder over Idriç contrib JSON |
| role-group JSON decoding | same | typed `/job_role_groups` decoder over the same parsed JSON |
| environment access | `System.getEnv : ... → Maybe String` | direct wrapper for fixed valid names |
| decimal job-id validation | ordinary character/list operations | nonempty ASCII decimal check |
| ICU GET with `x-api-key` | ICU #13 header/capture mechanics plus ICU #20 caller-declared credential headers | implemented; pending that ICU stack landing |

`Language.JSON` is already part of current Idriç contrib. It parses a string to `Maybe JSON`, with structural `JNull`, `JBoolean`, `JNumber`, `JString`, `JArray`, and `JObject` values plus object-field lookup. The Handshake checkpoint therefore does not carry its own JSON grammar. Only fields needed for the two raw tables are decoded into Handshake records; unrelated response fields remain parsed JSON and are ignored.

`Language.JSON` represents JSON numbers as `Double`. Required Handshake identifier fields are accepted only when the parsed number converts back to the same integral value. If Handshake ever documents or emits identifiers outside the exactly representable integer range of that JSON surface, that becomes a real decoding-surface limitation rather than something this client should hide.

Idriç PR #67 is separately restoring the stricter project-level `environment_value : String → IO (Maybe String)` wrapper after the source-layout rewrite. Handshake does not need to block on it: this client queries fixed valid names and current `System.getEnv` already returns `Maybe String`.

## Credential redirect boundary

ICU #13 supplies validated caller headers and `fetch_to_files_with_headers`. ICU #20, stacked directly on #13, adds `make_credential_header` and carries only the declared credential header names into native redirect handling.

For Handshake the request is therefore constructed as a normal GET with a credential-tagged `x-api-key`. On same-origin redirects the full request header survives. On cross-origin redirects ICU removes `Authorization`, `Cookie`, and all caller-declared credential headers while preserving unrelated custom headers and rewriting `Host`.

Handshake does not invent a private transport. It does not substitute curl, Python, browser cookies, or a Handshake-specific socket path.

## Checkpoint ladder

1. source parses/checks against current Idriç, contrib, and the current ICU header/capture stack;
2. `url jobs` and `url roles` print the documented endpoints;
3. `public JOB_ID` accepts decimal ids and rejects malformed ids;
4. jobs fixture decodes, flattens embedded TSV-breaking whitespace, and matches its TSV receipt;
5. job-role-group fixture decodes and matches its TSV receipt;
6. process environment distinguishes missing and empty API keys;
7. ICU proves caller-declared credential headers survive same-origin redirects and are stripped cross-origin;
8. the Handshake executable traverses a deterministic same-origin then cross-origin redirect chain with synthetic `x-api-key` and returns the jobs fixture only if both header conditions hold;
9. one actual `/jobs` page can be accepted with an authorized EDU key;
10. one actual `/job_role_groups` page can be accepted through the same boundary;
11. cursor pagination is added without changing the one-page decoder contract;
12. a separate analysis command joins jobs and role groups by `job_id` and reports classification evidence without altering raw observations.

## Public-catalog follow-up

The public `find-jobs` catalog is useful to an individual job seeker even without institutional EDU API credentials. It should be investigated as a separate checkpoint. Only add a machine-readable public-catalog client if there is a stable public interface whose use can be documented; do not infer a private student API from browser internals.
Loading
Loading