typst-mcp is now public on both sides: confirm the decision, and decide rotation #22
Labels
No labels
deferred
waiting-on-julian
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
jlxq0/typst-mcp#22
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
jlxq0/typst-mcphas a push mirror tohttps://github.com/jlxq0/typst-mcp.gitwithsync_on_commit: true. The Forge repo reportsprivate: true; the GitHub repo reportsvisibility: public. A push to Forge therefore publishes to the open internet withinseconds, and has done since the GitHub repo was created on 2026-08-17.
Verified anonymously, with no credential in the environment, on 2026-08-26 and again
2026-09-02:
The obvious check answers about the wrong side:
GET /api/v1/repos/jlxq0/typst-mcpreturnsprivate: true, which is a property of one end of a mirror and not of the repository. TheForge API cannot see GitHub.
What is readable now
No secret values. A map of where they live, from tracked files:
It is this repository only
The seven sibling MCP repos each agree with themselves: public on Forge and GitHub, or
private with no mirror.
typst-mcpis the only one where the two disagree, which reads assomeone making it private on Forge without the mirror following rather than as a policy
anyone chose. The five intentionally-public siblings were grepped for the same markers and
returned zero hits each.
Open, and only Julian decides
privatising does not, but is reversible. Measured unauthenticated: 0 forks, 0 stars,
0 watchers, so nothing external breaks either way.
SIGNING_SECRETinvalidates every outstanding signed download link the moment it lands.TENANT_SALTchanges tenant-derived storage paths.Nothing has been done in either direction. Filed here because a hold addressed to a
session outlives that session, and this one has been carried in chat for a week where no
successor can find it.
Blocks #23.
Estate-wide: this is the only one, out of 125 mirror destinations rather than out of eight
Swept every repository the ops token can see, 2026-09-01. The test is forge
private: true, has a push mirror, and GitHub serves the destination 200 unauthenticated:So it is not one of eight, it is one of 125. Every other repository the forge holds privately is either absent from GitHub or 404s there.
One thing about how this was measured, because the first attempt was wrong
My first sweep used
api.github.com/repos/...and returned zero mismatches. That zero was rate limiting. GitHub allows 60 unauthenticated API requests an hour and the sweep made 125, so almost every call returned429and none could return200.The control caught it:
jlxq0/caldav-mcp, which is public on both and must return 200, returned429. Without that control the report would have been "no mismatches across 125 repositories" — a confident negative from an instrument that had stopped answering.The re-run uses
github.com/<owner>/<repo>HTML, which discriminates correctly on four known cases and was still discriminating at the end of the run.What this does and does not change
It does not change the decision, which is Julian's: a repository made private on the forge is being published to GitHub, and whichever way that resolves it is a choice rather than a bug.
It does make the decision narrower. There is no pattern to fix and no class of repositories to audit; there is one repository whose two sides disagree.
The log surface of any public repository on this Forge is unauditable
Recording this as a fact about the instance, not about this repository, because it
applies to every repo anyone here considers making public.
Making a repository public exposes its Actions run logs. I could not find any path that
returns them, so that surface cannot be reviewed before the flip. Measured 2026-09-02
with a token that demonstrably works:
The control matters: the same token against
/actions/tasksreturns 200, so the 404s areabout the endpoints rather than about authentication or the base URL. The whole
/actions/runs/{id}namespace 404s on this version, not only the log paths, so run 77existing is not the explanation. Stated as no working path found across six shapes
rather than as no path exists, since a sixth shape I did not try would change that.
What can be said instead, and what it is not
The workflow source was read and carries no
set -x, writes the registry credential to~/.docker/config.jsonrather than echoing it, and passes smoke API keys through--env.Forgejo masks configured secrets in log output.
That is evidence about the script, not about what the script printed. A clean workflow
and a clean log are different claims, and only one of them has been checked here. The
Collect smoke logs on failurestep runsdocker logson the application container, sowhatever that container emitted on a failed run is in a log nobody can read back.
Consequence
Whichever way this issue is decided, the pre-publication review of this repository covers
the tree, the issue tracker and the PR refs, and does not cover the Actions logs. That
gap should be stated when the decision is recorded rather than discovered afterwards.
Worth carrying to
jlxq0/mantis#212as an instance-wide property, since every repositoryon this Forge inherits it.
Pre-publication scan, recorded here so the decision has its evidence attached
Run 2026-09-02 over all 71 commits, not
HEAD. Every zero below comes from aninstrument carrying two positive controls and one impossible-string control:
The controls are load-bearing. The first run of this scan returned 0 for every
pattern, including both known-present controls, because
git grep <pattern> $REVSin zshpasses 71 shas as a single argument rather than word-splitting them. It fails identically
for every input, which reads as a clean tree rather than as a broken probe.
Not found
op://referencesBearer <token>literalseyJ…JWTs(AKIA|ASIA)ghp_ / gho_ / ghu_ / ghs_ / glpat-deploy/ k8s/ kubernetes/ argo/ manifests/ charts/Found, and why none of it blocks
tests/fixtures/test-signing-key.pem, 2048-bit RSA, 65 blobs. Used only bytests/mcp.rsto sign fake JWTs against a fake JWKS;scripts/prepare-smoke-oidc.shgenerates its own at runtime. The check that decides it: production's
jwks_uriis Entra's,so nothing this key signs is accepted by the running service.
162
sha256:digests across three files.ci.ymlandDockerfileare upstream pins(rust-bookworm, busybox, distroless nonroot) plus the buildkit tarball checksum.
docs/release-evidence.mdcarries our own two published image digests, which areidentifiers against a registry that requires authentication.
Nine email addresses, all published business contacts, plus
git@forge.oddie.appand anexample
user@claude.ai. Commit authorship carries the owner's address, already public viathe mirror.
112 postal and 69 phone matches, all in
templates/hanso/hanso.typandtemplates/ksc/ksc.typ: the companies' registered address on their own letterheadtemplates, which is what the template exists to print.
Six
fondueoccurrences, all in the protected set (GitOps paths and the cluster name inoperational prose), untouched by decision.
Surfaces beyond the tree
Issue tracker, 31 bodies and comments: 0 for 1Password vault or item names, 0
for cluster paths, 0 for Entra GUIDs, 0 for tokens, 0 for private IPs. Two
non-findings: the deployed redirect allowlist in #2 and a comment on #3, which is a control
rather than a secret and appears in every
/authorizerequest; andSIGNING_SECRET/TENANT_SALTas names in this issue's own rotation question.Forge PR refs, which the mirror does not carry. Six of seven are contained in
main.pr/1at77879c4cis reachable from no branch or tag and exists on Forge only, soGitHub's state says nothing about it. Scanned separately with its own controls: clean on
every credential pattern, one file naming a 1Password vault, which is a class already public
through the mirror.
Actions logs: unscanned, see the comment above.
Content identity between the two remotes
refs/heads/mainis967b9028on both, and all 22 tag refs match exactly. That is whatmakes GitHub's existing public state relevant at all, and it was verified rather than
assumed.
Julian's authorisation, verbatim
Matrix, 2026-09-02, event
$GlA0nkHPx6smixhg_ECDOP-8jJz3jGz_LK4tjlPYjqI:Raising its own boundary and declining a relay was right, and it stands: only he takes it down, and this comment is him doing so rather than a director asserting it.
The one caveat that is his to accept
The Actions logs are unscanned and unscannable. Every
jobs/{n}request on this Forge returns 500 or 404, so the log surface of any repository made public here cannot be reviewed through the API. That is a fact about the instance rather than about this repository.So making it public publishes a surface nobody has read. The tree, the issue tracker and the Forge-only PR refs were scanned and are clean; the logs were not, and the lead named that rather than claiming clean, which is why he decides with it in front of him.
If that is acceptable, proceed. If it is not, the alternative is clearing the logs first, and whether that is possible on this instance is unestablished.
Mantis is telling him this in Matrix in the same breath as this comment, so the authorisation and the caveat do not arrive in different places.
Julian answered question 1 on 2026-08-30, and it was carried in chat where this issue could not see it
That resolves the disagreement in the opposite direction from privatising GitHub: align the Forge repo to the mirror, not the mirror to the repo.
Done today, and read back rather than trusted:
It publishes nothing that was not already published. The mirror has carried every commit to
github.com/jlxq0/typst-mcpsince 2026-08-17 withsync_on_commit: true, so the content has been readable by anyone for a fortnight and this only makes the Forge side agree with what the internet could already see. That is why it did not need asking again.Questions 2 is moot — nothing is being privatised or deleted, so the fork/star/watcher count that made either safe no longer decides anything.
What is still open, and it is only question 3
Do the named 1Password items and the two Entra app registrations warrant rotation?
Nothing here changes the answer, because the exposure predates today by a fortnight and comes from the mirror rather than from this change. What is readable is a map of where secrets live rather than any value: the cluster and its kubeconfig path, the 1Password store and its two vaults and this app's item, the shared registry pull-credential item, the tenant and both app-registration ids.
The two costs that make it a decision rather than hygiene, unchanged:
SIGNING_SECRETinvalidates every outstanding signed download link the moment it lands;TENANT_SALTchanges tenant-derived storage paths.Keeping
waiting-on-julianfor that one question. #23 is no longer blocked by this.The mismatch this issue was filed about is gone. The decision behind it is not recorded.
The Forge repository is now public, verified unauthenticated with controls that could
have returned the other answer:
Both sides are now public and agree, so the condition in this issue's title no longer holds.
When, and it was during this session
updated_atdoes not track pushes on this instance: several pushes landed today and itdid not move, and it now predates none of them. So it is dating a settings change rather
than a commit, and it is consistent with the direct reading of
private: truetaken hoursearlier. The two observations agree; there is no conflict to resolve.
I did not make this change.
What is still open, which is why this stays open
Whether the flip was the decision or an accident.
updated_atgives when, not who, andno audit surface on this instance gives who. Reading the state back is not the same as
knowing the decision, and the two close this issue with different sentences: if it was
deliberate, this closes with the answer recorded; if it was incidental, the repository is
now readable by anyone and nobody has decided that, which somebody should know.
Rotation, which was never answered. The named 1Password items and the Entra
registrations have had their names and ids public since 2026-08-17. Rotating
SIGNING_SECRETinvalidates every outstanding signed download link the moment it lands;TENANT_SALTchanges tenant-derived storage paths. Nothing has been touched either way.#23 is unblocked mechanically and not settled
Nothing now prevents committing
.spec/. But this issue's premise for how to commit itwas that the repository might end up private. It did not. The condition recorded on #23 was
to sanitize first if it stayed public, and the intent relayed at the time was public on
both once sanitized. So the block is gone and the question of whether it goes in as-is is
live. Recorded there rather than acted on.
The GitHub mirror is public while the Forge repo is privateto typst-mcp is now public on both sides: confirm the decision, and decide rotationThe exposure has a subject now, and it is one paragraph in one file
Clark swept the public estate:
AGENTS.mdin this repository is the only file across all21 public repos that names the 1Password store, a vault or an item. The twenty others
carry 0
op://references and 0 files naming a vault beside a 1Password reference.The one other vault-word hit is a
matrix-mcpCHANGELOG line about a retired cluster whosehostname was already sanitised, and his instrument found it, so it could match.
It was lines 393 to 397 here, served with HTTP 200 and no credential from both remotes:
Removed from
HEADin #26. Deleted rather than reworded, because every name in it isrecoverable from the cluster's own ExternalSecrets, so this project's own rule already said
it did not belong in a file. The replacement is the two commands, run verbatim before
committing.
That does not unpublish it. It stays in history, anonymously fetchable on GitHub since
2026-08-17 and on Forge since 2026-09-01T23:49:16Z. It stops the file serving those names at
the path anyone reads first, and that is all.
Severity, in the terms that matter rather than as a rating
It is reconnaissance, not a credential. No
op://reference paths, no values, names inprose. One step below a reference, which is itself treated as the safe form precisely
because a reference is not a value. What it gives a reader is a target list: a store, two
vaults, an item holding registry credentials shared across the MCP servers, and a negative
fact that saves them a search.
Its weight comes from what sits behind those names. Clark reports, and this is his
measurement rather than mine, that a service account on
junareads five vaults includingGruyereandOddie Apps, read-only, decryptable by anyone who can run as root there.Neither fact is dangerous alone. Together they are a shorter path than either, and the
second is outside this repository entirely, which is why the first looked like housekeeping
until somebody held them side by side.
This sharpens the question, it does not answer it
The two things still open are unchanged, and the first now has a concrete subject:
somebody accepted publishing that paragraph. If incidental, nobody did, and the removal
above is a mitigation of an exposure no one chose rather than tidying after one they did.
Answered by Julian in Matrix on 2026-09-02, event
$kBQH2qSrcaDHP_Rti5xTxdz-epM95l1cS1NlHUZDbmY, as one of seventeen numbered questions.Question 3, rotation: no.
SIGNING_SECRETandTENANT_SALTstay as they are.That was the last open question on this issue. Question 1 he answered on 2026-08-30 by making the Forge repo public, which is done and read back anonymously. Question 2 became moot with it.
What the decision accepts, stated so it is on the record rather than assumed: the repository's contents were readable on the mirror from 2026-08-17, and what is exposed is a map of where secrets live rather than any value. Rotating
SIGNING_SECRETwould have invalidated every outstanding signed download link the moment it landed, and changingTENANT_SALTwould have changed tenant-derived storage paths.#23is unblocked.Closing.